kumo-svelte 0.12.0 → 0.13.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/ai/component-registry.json +112 -2
- package/ai/component-registry.md +1 -1
- package/dist/components/combobox/ComboboxInput.svelte +1 -1
- package/dist/components/combobox/ComboboxTest.svelte +2 -0
- package/dist/components/combobox/combobox.test.js +9 -0
- package/dist/components/dialog/Dialog.svelte +3 -7
- package/dist/components/dialog/Dialog.svelte.d.ts +1 -1
- package/dist/components/dialog/Dialog.variants.d.ts +4 -0
- package/dist/components/dialog/Dialog.variants.js +4 -0
- package/dist/components/dialog/dialog.test.js +8 -1
- package/dist/components/dialog/index.d.ts +2 -1
- package/dist/components/dialog/index.js +1 -1
- package/dist/components/input-group/context.js +1 -1
- package/dist/components/input-group/input-group.test.js +4 -0
- package/dist/components/pagination/Pagination.svelte +6 -0
- package/dist/components/pagination/Pagination.svelte.d.ts +2 -0
- package/dist/components/pagination/PaginationControls.svelte +13 -5
- package/dist/components/pagination/context.d.ts +1 -0
- package/dist/components/pagination/pagination.test.js +19 -0
- package/dist/components/select/Select.svelte +40 -3
- package/dist/components/select/Select.svelte.d.ts +15 -0
- package/dist/components/select/select-variants.d.ts +2 -2
- package/dist/components/select/select-variants.js +3 -3
- package/dist/components/select/select.test.js +103 -81
- package/dist/components/table/Table.svelte +1 -1
- package/dist/components/table/Table.svelte.d.ts +1 -1
- package/dist/components/table/table.test.js +5 -0
- package/dist/components/text/Text.svelte +4 -4
- package/dist/components/text/Text.svelte.d.ts +5 -5
- package/dist/components/text/text.test.js +12 -1
- package/dist/components/toolbar/ToolbarLink.svelte +48 -0
- package/dist/components/toolbar/ToolbarLink.svelte.d.ts +20 -0
- package/dist/components/toolbar/ToolbarTestHost.svelte +6 -0
- package/dist/components/toolbar/index.d.ts +4 -1
- package/dist/components/toolbar/index.js +3 -1
- package/dist/components/toolbar/toolbar.test.js +15 -1
- package/dist/components/tooltip/Tooltip.svelte +1 -1
- package/dist/components/tooltip/TooltipTestHost.svelte +4 -0
- package/dist/components/tooltip/tooltip.test.js +6 -0
- package/dist/docs/demo-snippets/PaginationUnknownTotalDemo.svelte +12 -0
- package/dist/docs/demo-snippets/PaginationUnknownTotalDemo.svelte.d.ts +3 -0
- package/dist/docs/demo-snippets/SelectPlacementDemo.svelte +20 -0
- package/dist/docs/demo-snippets/SelectPlacementDemo.svelte.d.ts +18 -0
- package/dist/docs/demo-snippets/TabsDefaultDemo.svelte +1 -1
- package/dist/docs/demo-snippets/TabsSmDemo.svelte +1 -1
- package/dist/docs/demo-snippets/ToolbarLinksDemo.svelte +10 -0
- package/dist/docs/demo-snippets/ToolbarLinksDemo.svelte.d.ts +18 -0
- package/dist/docs/props-data/Pagination.js +6 -0
- package/dist/docs/props-data/Select.js +52 -0
- package/dist/docs/props-data/Toolbar__Link.d.ts +6 -0
- package/dist/docs/props-data/Toolbar__Link.js +32 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
- package/src/lib/components/combobox/ComboboxInput.svelte +1 -1
- package/src/lib/components/combobox/ComboboxTest.svelte +2 -0
- package/src/lib/components/combobox/combobox.test.ts +11 -0
- package/src/lib/components/dialog/Dialog.svelte +3 -7
- package/src/lib/components/dialog/Dialog.variants.ts +12 -0
- package/src/lib/components/dialog/dialog.test.ts +9 -1
- package/src/lib/components/dialog/index.ts +2 -1
- package/src/lib/components/input-group/context.ts +1 -1
- package/src/lib/components/input-group/input-group.test.ts +5 -0
- package/src/lib/components/pagination/Pagination.svelte +6 -0
- package/src/lib/components/pagination/PaginationControls.svelte +13 -5
- package/src/lib/components/pagination/context.ts +1 -0
- package/src/lib/components/pagination/pagination.test.ts +23 -0
- package/src/lib/components/select/Select.svelte +40 -3
- package/src/lib/components/select/select-variants.ts +3 -3
- package/src/lib/components/select/select.test.ts +111 -81
- package/src/lib/components/table/Table.svelte +1 -1
- package/src/lib/components/table/table.test.ts +12 -0
- package/src/lib/components/text/Text.svelte +4 -4
- package/src/lib/components/text/text.test.ts +13 -1
- package/src/lib/components/toolbar/ToolbarLink.svelte +48 -0
- package/src/lib/components/toolbar/ToolbarTestHost.svelte +6 -0
- package/src/lib/components/toolbar/index.ts +5 -0
- package/src/lib/components/toolbar/toolbar.test.ts +17 -0
- package/src/lib/components/tooltip/Tooltip.svelte +1 -1
- package/src/lib/components/tooltip/TooltipTestHost.svelte +4 -0
- package/src/lib/components/tooltip/tooltip.test.ts +7 -0
- package/src/lib/docs/demo-snippets/PaginationUnknownTotalDemo.svelte +12 -0
- package/src/lib/docs/demo-snippets/SelectPlacementDemo.svelte +20 -0
- package/src/lib/docs/demo-snippets/TabsDefaultDemo.svelte +1 -1
- package/src/lib/docs/demo-snippets/TabsSmDemo.svelte +1 -1
- package/src/lib/docs/demo-snippets/ToolbarLinksDemo.svelte +10 -0
- package/src/lib/docs/props-data/Pagination.ts +6 -0
- package/src/lib/docs/props-data/Select.ts +52 -0
- package/src/lib/docs/props-data/Toolbar__Link.ts +32 -0
- package/src/lib/index.ts +2 -0
- package/src/routes/components/pagination/+page.md +17 -0
- package/src/routes/components/select/+page.md +41 -63
- package/src/routes/components/toolbar/+page.md +13 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.13.0",
|
|
3
3
|
"components": {
|
|
4
4
|
"Autocomplete": {
|
|
5
5
|
"name": "Autocomplete",
|
|
@@ -3934,6 +3934,11 @@
|
|
|
3934
3934
|
"optional": true,
|
|
3935
3935
|
"description": "Total number of items across all pages."
|
|
3936
3936
|
},
|
|
3937
|
+
"hasNextPage": {
|
|
3938
|
+
"type": "boolean",
|
|
3939
|
+
"optional": true,
|
|
3940
|
+
"description": "Whether another page exists when the total count is unknown."
|
|
3941
|
+
},
|
|
3937
3942
|
"class": {
|
|
3938
3943
|
"type": "string",
|
|
3939
3944
|
"optional": true,
|
|
@@ -4639,11 +4644,76 @@
|
|
|
4639
4644
|
"type": "HTMLElement | string",
|
|
4640
4645
|
"optional": true,
|
|
4641
4646
|
"description": "Container element for the portal."
|
|
4647
|
+
},
|
|
4648
|
+
"side": {
|
|
4649
|
+
"type": "enum",
|
|
4650
|
+
"optional": true,
|
|
4651
|
+
"values": [
|
|
4652
|
+
"top",
|
|
4653
|
+
"right",
|
|
4654
|
+
"bottom",
|
|
4655
|
+
"left"
|
|
4656
|
+
],
|
|
4657
|
+
"default": "bottom",
|
|
4658
|
+
"description": "Preferred popup side; collision handling may flip it."
|
|
4659
|
+
},
|
|
4660
|
+
"sideOffset": {
|
|
4661
|
+
"type": "number",
|
|
4662
|
+
"optional": true,
|
|
4663
|
+
"default": "4",
|
|
4664
|
+
"description": "Distance between the trigger and popup."
|
|
4665
|
+
},
|
|
4666
|
+
"align": {
|
|
4667
|
+
"type": "enum",
|
|
4668
|
+
"optional": true,
|
|
4669
|
+
"values": [
|
|
4670
|
+
"start",
|
|
4671
|
+
"center",
|
|
4672
|
+
"end"
|
|
4673
|
+
],
|
|
4674
|
+
"default": "start",
|
|
4675
|
+
"description": "Popup alignment along the trigger edge."
|
|
4676
|
+
},
|
|
4677
|
+
"alignOffset": {
|
|
4678
|
+
"type": "number",
|
|
4679
|
+
"optional": true,
|
|
4680
|
+
"default": "0",
|
|
4681
|
+
"description": "Additional offset along the alignment axis."
|
|
4682
|
+
},
|
|
4683
|
+
"customAnchor": {
|
|
4684
|
+
"type": "HTMLElement | string",
|
|
4685
|
+
"optional": true,
|
|
4686
|
+
"description": "Custom floating-position anchor."
|
|
4687
|
+
},
|
|
4688
|
+
"strategy": {
|
|
4689
|
+
"type": "enum",
|
|
4690
|
+
"optional": true,
|
|
4691
|
+
"values": [
|
|
4692
|
+
"absolute",
|
|
4693
|
+
"fixed"
|
|
4694
|
+
],
|
|
4695
|
+
"description": "CSS positioning strategy for the popup."
|
|
4696
|
+
},
|
|
4697
|
+
"avoidCollisions": {
|
|
4698
|
+
"type": "boolean",
|
|
4699
|
+
"optional": true,
|
|
4700
|
+
"description": "Whether the popup should adjust to avoid viewport collisions."
|
|
4701
|
+
},
|
|
4702
|
+
"collisionPadding": {
|
|
4703
|
+
"type": "enum",
|
|
4704
|
+
"optional": true,
|
|
4705
|
+
"values": [
|
|
4706
|
+
"top",
|
|
4707
|
+
"right",
|
|
4708
|
+
"bottom",
|
|
4709
|
+
"left"
|
|
4710
|
+
],
|
|
4711
|
+
"description": "Padding between the popup and its collision boundary."
|
|
4642
4712
|
}
|
|
4643
4713
|
},
|
|
4644
4714
|
"colors": [
|
|
4645
4715
|
"bg-kumo-base",
|
|
4646
|
-
"bg-kumo-
|
|
4716
|
+
"bg-kumo-control",
|
|
4647
4717
|
"bg-kumo-hairline",
|
|
4648
4718
|
"bg-kumo-tint",
|
|
4649
4719
|
"border-kumo-line",
|
|
@@ -6258,6 +6328,46 @@
|
|
|
6258
6328
|
"description": "Additional classes merged onto the toolbar input group."
|
|
6259
6329
|
}
|
|
6260
6330
|
}
|
|
6331
|
+
},
|
|
6332
|
+
"Link": {
|
|
6333
|
+
"description": "Toolbar.Link",
|
|
6334
|
+
"props": {
|
|
6335
|
+
"children": {
|
|
6336
|
+
"type": "Snippet",
|
|
6337
|
+
"optional": true,
|
|
6338
|
+
"description": "Optional visible link label."
|
|
6339
|
+
},
|
|
6340
|
+
"href": {
|
|
6341
|
+
"type": "string",
|
|
6342
|
+
"optional": true,
|
|
6343
|
+
"description": "Navigation destination."
|
|
6344
|
+
},
|
|
6345
|
+
"icon": {
|
|
6346
|
+
"type": "Component",
|
|
6347
|
+
"optional": true,
|
|
6348
|
+
"description": "Optional icon component rendered before the label."
|
|
6349
|
+
},
|
|
6350
|
+
"shape": {
|
|
6351
|
+
"type": "enum",
|
|
6352
|
+
"optional": true,
|
|
6353
|
+
"values": [
|
|
6354
|
+
"base",
|
|
6355
|
+
"square",
|
|
6356
|
+
"circle"
|
|
6357
|
+
],
|
|
6358
|
+
"description": "Link shape. Icon-only toolbar links default to square."
|
|
6359
|
+
},
|
|
6360
|
+
"external": {
|
|
6361
|
+
"type": "boolean",
|
|
6362
|
+
"optional": true,
|
|
6363
|
+
"description": "Opens the link in a new tab with safe rel attributes."
|
|
6364
|
+
},
|
|
6365
|
+
"disabled": {
|
|
6366
|
+
"type": "boolean",
|
|
6367
|
+
"optional": true,
|
|
6368
|
+
"description": "Disables navigation and exposes disabled semantics."
|
|
6369
|
+
}
|
|
6370
|
+
}
|
|
6261
6371
|
}
|
|
6262
6372
|
}
|
|
6263
6373
|
},
|
package/ai/component-registry.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</script>
|
|
14
14
|
|
|
15
15
|
<input
|
|
16
|
-
class={cn(inputStyles[context.size], 'mx-1.5 w-
|
|
16
|
+
class={cn(inputStyles[context.size], 'mx-0 -mt-1.5 w-full shrink-0 rounded-b-none first:mb-2 border-0 bg-kumo-control text-kumo-default ring ring-kumo-line outline-none placeholder:text-kumo-muted focus:ring-kumo-focus/50 focus:ring-[1.5px]', className)}
|
|
17
17
|
value={context.query}
|
|
18
18
|
{placeholder}
|
|
19
19
|
disabled={context.disabled}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import ComboboxContent from './ComboboxContent.svelte';
|
|
5
5
|
import ComboboxList from './ComboboxList.svelte';
|
|
6
6
|
import ComboboxItem from './ComboboxItem.svelte';
|
|
7
|
+
import ComboboxInput from './ComboboxInput.svelte';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
9
10
|
value?: unknown;
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
hideWhenDetached={false}
|
|
26
27
|
updatePositionStrategy="optimized"
|
|
27
28
|
>
|
|
29
|
+
<ComboboxInput placeholder="Search fruit" />
|
|
28
30
|
<ComboboxList>
|
|
29
31
|
{#snippet children(item)}
|
|
30
32
|
<ComboboxItem value={item}>{item}</ComboboxItem>
|
|
@@ -3,6 +3,15 @@ import userEvent from "@testing-library/user-event";
|
|
|
3
3
|
import { describe, expect, it, vi } from "vitest";
|
|
4
4
|
import ComboboxTest from "./ComboboxTest.svelte";
|
|
5
5
|
describe("Combobox Keyboard Navigation", () => {
|
|
6
|
+
it("aligns popup input with the content edges", async () => {
|
|
7
|
+
const user = userEvent.setup();
|
|
8
|
+
render(ComboboxTest);
|
|
9
|
+
await user.click(screen.getByRole("button", { name: "Show options" }));
|
|
10
|
+
const input = screen.getByPlaceholderText("Search fruit");
|
|
11
|
+
expect(input.className).toContain("mx-0");
|
|
12
|
+
expect(input.className).toContain("-mt-1.5");
|
|
13
|
+
expect(input.className).toContain("rounded-b-none");
|
|
14
|
+
});
|
|
6
15
|
it("allows navigating and selecting items using Arrow keys and Enter", async () => {
|
|
7
16
|
const user = userEvent.setup();
|
|
8
17
|
const onValueChange = vi.fn();
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
<script module lang="ts">
|
|
2
|
-
export { KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
2
|
+
export { dialogVariants, KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
import { AlertDialog, Dialog as DialogPrimitive } from 'bits-ui';
|
|
7
7
|
import type { Snippet } from 'svelte';
|
|
8
8
|
import { cn } from '../../utils/cn';
|
|
9
|
-
import { KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
9
|
+
import { dialogVariants, KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
10
10
|
|
|
11
11
|
export const KUMO_DIALOG_STYLING = {
|
|
12
12
|
dimensions: {
|
|
@@ -104,11 +104,7 @@
|
|
|
104
104
|
'fixed inset-0 bg-kumo-recessed opacity-80 transition-all duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0';
|
|
105
105
|
|
|
106
106
|
let contentClasses = $derived(
|
|
107
|
-
cn(
|
|
108
|
-
'shadow-m ring ring-kumo-line fixed top-1/2 left-1/2 w-full max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-hidden rounded-xl bg-kumo-base text-kumo-default duration-150 data-ending-style:scale-90 data-ending-style:opacity-0 data-starting-style:scale-90 data-starting-style:opacity-0',
|
|
109
|
-
KUMO_DIALOG_VARIANTS.size[size].classes,
|
|
110
|
-
className
|
|
111
|
-
)
|
|
107
|
+
cn(dialogVariants({ size }), className)
|
|
112
108
|
);
|
|
113
109
|
|
|
114
110
|
const contentStyle = $derived(
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
1
|
+
export { dialogVariants, KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
2
2
|
import type { Snippet } from 'svelte';
|
|
3
3
|
import { KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
4
4
|
type DialogSize = keyof typeof KUMO_DIALOG_VARIANTS.size;
|
|
@@ -32,3 +32,7 @@ export declare const KUMO_DIALOG_DEFAULT_VARIANTS: {
|
|
|
32
32
|
readonly size: "base";
|
|
33
33
|
readonly role: "dialog";
|
|
34
34
|
};
|
|
35
|
+
export type KumoDialogSize = keyof typeof KUMO_DIALOG_VARIANTS.size;
|
|
36
|
+
export declare function dialogVariants({ size }?: {
|
|
37
|
+
size?: KumoDialogSize;
|
|
38
|
+
}): string;
|
|
@@ -32,3 +32,7 @@ export const KUMO_DIALOG_DEFAULT_VARIANTS = {
|
|
|
32
32
|
size: 'base',
|
|
33
33
|
role: 'dialog'
|
|
34
34
|
};
|
|
35
|
+
export function dialogVariants({ size = KUMO_DIALOG_DEFAULT_VARIANTS.size } = {}) {
|
|
36
|
+
return cn('shadow-m ring ring-kumo-line fixed top-8 left-1/2 w-full max-w-[calc(100vw-2rem)] -translate-x-1/2 overflow-hidden rounded-xl bg-kumo-base text-kumo-default duration-150 data-ending-style:scale-90 data-ending-style:opacity-0 data-starting-style:scale-90 data-starting-style:opacity-0 sm:top-16', KUMO_DIALOG_VARIANTS.size[size].classes);
|
|
37
|
+
}
|
|
38
|
+
import { cn } from '../../utils/cn';
|
|
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import { expectNoA11yViolations } from "../../../../tests/a11y";
|
|
5
5
|
import DialogTestHost from "./DialogTestHost.svelte";
|
|
6
|
-
import { KUMO_DIALOG_VARIANTS } from "./Dialog.variants";
|
|
6
|
+
import { dialogVariants, KUMO_DIALOG_VARIANTS } from "./Dialog.variants";
|
|
7
7
|
describe("Dialog", () => {
|
|
8
8
|
it("renders the trigger with Kumo data attributes", () => {
|
|
9
9
|
render(DialogTestHost);
|
|
@@ -54,6 +54,13 @@ describe("Dialog", () => {
|
|
|
54
54
|
});
|
|
55
55
|
});
|
|
56
56
|
describe("Dialog variants", () => {
|
|
57
|
+
it("positions dialogs closer to the top of the viewport", () => {
|
|
58
|
+
const classes = dialogVariants().split(" ");
|
|
59
|
+
expect(classes).toContain("top-8");
|
|
60
|
+
expect(classes).toContain("sm:top-16");
|
|
61
|
+
expect(classes).not.toContain("top-1/2");
|
|
62
|
+
expect(classes).not.toContain("-translate-y-1/2");
|
|
63
|
+
});
|
|
57
64
|
it("uses fixed width classes for size variants", () => {
|
|
58
65
|
for (const [size, expectedClass] of [
|
|
59
66
|
["sm", "sm:w-72"],
|
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
export { default as Dialog } from './Dialog.svelte';
|
|
2
|
-
export { KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
2
|
+
export { dialogVariants, KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
3
|
+
export type { KumoDialogSize } from './Dialog.variants';
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { default as Dialog } from './Dialog.svelte';
|
|
2
|
-
export { KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
2
|
+
export { dialogVariants, KUMO_DIALOG_DEFAULT_VARIANTS, KUMO_DIALOG_VARIANTS } from './Dialog.variants';
|
|
@@ -41,7 +41,7 @@ export const INPUT_GROUP_SIZE = {
|
|
|
41
41
|
addonOuterStart: 'pl-2.5',
|
|
42
42
|
addonOuterEnd: 'pr-2.5',
|
|
43
43
|
addonButtonOuterStart: 'pl-1.5',
|
|
44
|
-
addonButtonOuterEnd: 'pr-
|
|
44
|
+
addonButtonOuterEnd: 'pr-0.5',
|
|
45
45
|
suffixPad: 'pr-4',
|
|
46
46
|
fontSize: 'text-base',
|
|
47
47
|
addonIconSize: '[&>svg]:size-5',
|
|
@@ -3,7 +3,11 @@ import userEvent from '@testing-library/user-event';
|
|
|
3
3
|
import { describe, expect, it } from 'vitest';
|
|
4
4
|
import { expectNoA11yViolations } from '../../../../tests/a11y';
|
|
5
5
|
import InputGroupTestHost from './InputGroupTestHost.svelte';
|
|
6
|
+
import { INPUT_GROUP_SIZE } from './context';
|
|
6
7
|
describe('InputGroup', () => {
|
|
8
|
+
it('uses compact end spacing for a button in a large group', () => {
|
|
9
|
+
expect(INPUT_GROUP_SIZE.lg.addonButtonOuterEnd).toBe('pr-0.5');
|
|
10
|
+
});
|
|
7
11
|
it('renders input inside group', () => {
|
|
8
12
|
render(InputGroupTestHost);
|
|
9
13
|
expect(screen.getByRole('textbox', { name: 'Search' })).toBeTruthy();
|
|
@@ -50,6 +50,8 @@
|
|
|
50
50
|
setPage?: (page: number) => void;
|
|
51
51
|
perPage?: number;
|
|
52
52
|
totalCount?: number;
|
|
53
|
+
/** Whether another page exists when no total count or page count is available. */
|
|
54
|
+
hasNextPage?: boolean;
|
|
53
55
|
pages?: number;
|
|
54
56
|
controls?: Controls;
|
|
55
57
|
text?: (props: InfoTextProps) => unknown;
|
|
@@ -64,6 +66,7 @@
|
|
|
64
66
|
setPage: setPageProp,
|
|
65
67
|
perPage,
|
|
66
68
|
totalCount,
|
|
69
|
+
hasNextPage,
|
|
67
70
|
pages,
|
|
68
71
|
controls = KUMO_PAGINATION_DEFAULT_VARIANTS.controls,
|
|
69
72
|
text,
|
|
@@ -101,6 +104,9 @@
|
|
|
101
104
|
get totalCount() {
|
|
102
105
|
return effectiveTotalCount;
|
|
103
106
|
},
|
|
107
|
+
get hasNextPage() {
|
|
108
|
+
return hasNextPage;
|
|
109
|
+
},
|
|
104
110
|
get maxPage() {
|
|
105
111
|
return maxPage;
|
|
106
112
|
},
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
setPage?: (page: number) => void;
|
|
15
15
|
perPage?: number;
|
|
16
16
|
totalCount?: number;
|
|
17
|
+
/** Whether another page exists when no total count or page count is available. */
|
|
18
|
+
hasNextPage?: boolean;
|
|
17
19
|
pages?: number;
|
|
18
20
|
controls?: Controls;
|
|
19
21
|
text?: (props: InfoTextProps) => unknown;
|
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
|
|
21
21
|
let { controls = 'full', pageSelector = 'input', class: className, ...rest }: Props = $props();
|
|
22
22
|
const context = getPaginationContext();
|
|
23
|
+
const hasKnownTotal = $derived(context.totalCount !== undefined);
|
|
24
|
+
const isUnknownTotal = $derived(!hasKnownTotal && context.hasNextPage !== undefined);
|
|
25
|
+
const showFullControls = $derived(controls === 'full' && !isUnknownTotal);
|
|
26
|
+
const isNextDisabled = $derived(
|
|
27
|
+
hasKnownTotal ? context.page === context.maxPage : context.hasNextPage !== true
|
|
28
|
+
);
|
|
23
29
|
const pageOptions = $derived(
|
|
24
30
|
Array.from({ length: context.maxPage }, (_, index) => {
|
|
25
31
|
const page = index + 1;
|
|
@@ -28,7 +34,9 @@
|
|
|
28
34
|
);
|
|
29
35
|
|
|
30
36
|
function commitPage(nextPage: number) {
|
|
31
|
-
const clamped =
|
|
37
|
+
const clamped = hasKnownTotal
|
|
38
|
+
? clamp(nextPage, 1, context.maxPage)
|
|
39
|
+
: Math.max(nextPage, 1);
|
|
32
40
|
context.setPage(clamped);
|
|
33
41
|
context.setEditingPage(clamped);
|
|
34
42
|
}
|
|
@@ -42,7 +50,7 @@
|
|
|
42
50
|
<div data-slot="pagination-controls" class={cn('grow flex flex-col items-end', className)} {...rest}>
|
|
43
51
|
<nav aria-label={context.labels.navigation}>
|
|
44
52
|
<InputGroup focusMode="individual">
|
|
45
|
-
{#if
|
|
53
|
+
{#if showFullControls}
|
|
46
54
|
<InputGroupButton
|
|
47
55
|
variant="secondary"
|
|
48
56
|
aria-label={context.labels.firstPage}
|
|
@@ -60,7 +68,7 @@
|
|
|
60
68
|
>
|
|
61
69
|
<CaretLeft class="size-4" />
|
|
62
70
|
</InputGroupButton>
|
|
63
|
-
{#if
|
|
71
|
+
{#if showFullControls}
|
|
64
72
|
{#if pageSelector === 'dropdown'}
|
|
65
73
|
<Select
|
|
66
74
|
aria-label={context.labels.pageNumber}
|
|
@@ -95,12 +103,12 @@
|
|
|
95
103
|
<InputGroupButton
|
|
96
104
|
variant="secondary"
|
|
97
105
|
aria-label={context.labels.nextPage}
|
|
98
|
-
disabled={
|
|
106
|
+
disabled={isNextDisabled}
|
|
99
107
|
onclick={() => commitPage(context.page + 1)}
|
|
100
108
|
>
|
|
101
109
|
<CaretRight class="size-4" />
|
|
102
110
|
</InputGroupButton>
|
|
103
|
-
{#if
|
|
111
|
+
{#if showFullControls}
|
|
104
112
|
<InputGroupButton
|
|
105
113
|
variant="secondary"
|
|
106
114
|
aria-label={context.labels.lastPage}
|
|
@@ -44,6 +44,25 @@ describe('Pagination', () => {
|
|
|
44
44
|
await user.click(screen.getByRole('button', { name: 'Next page' }));
|
|
45
45
|
expect(onPageChange).toHaveBeenCalledWith(3);
|
|
46
46
|
});
|
|
47
|
+
it('supports sequential pagination when the total is unknown', async () => {
|
|
48
|
+
const user = userEvent.setup();
|
|
49
|
+
const setPage = vi.fn();
|
|
50
|
+
render(Pagination, {
|
|
51
|
+
page: 3,
|
|
52
|
+
hasNextPage: true,
|
|
53
|
+
setPage,
|
|
54
|
+
text: ({ page }) => `Page ${page}`
|
|
55
|
+
});
|
|
56
|
+
expect(screen.queryByRole('button', { name: 'First page' })).toBeNull();
|
|
57
|
+
expect(screen.queryByRole('textbox', { name: 'Page number' })).toBeNull();
|
|
58
|
+
expect(screen.queryByRole('button', { name: 'Last page' })).toBeNull();
|
|
59
|
+
await user.click(screen.getByRole('button', { name: 'Next page' }));
|
|
60
|
+
expect(setPage).toHaveBeenCalledWith(4);
|
|
61
|
+
});
|
|
62
|
+
it('disables next when an unknown-total list has no following page', () => {
|
|
63
|
+
render(Pagination, { page: 3, hasNextPage: false });
|
|
64
|
+
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
|
|
65
|
+
});
|
|
47
66
|
it('updates the page from the dropdown selector', async () => {
|
|
48
67
|
const user = userEvent.setup();
|
|
49
68
|
const setPage = vi.fn();
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
type ItemDescriptor = string | { label: string; disabled?: boolean };
|
|
21
21
|
type Items = Record<string, ItemDescriptor> | Option[];
|
|
22
|
+
type ContentProps = SelectPrimitive.ContentProps;
|
|
22
23
|
|
|
23
24
|
interface Props {
|
|
24
25
|
class?: string;
|
|
@@ -45,6 +46,19 @@
|
|
|
45
46
|
renderValue?: (value: Value) => unknown;
|
|
46
47
|
onValueChange?: (value: Value) => void;
|
|
47
48
|
container?: HTMLElement | string;
|
|
49
|
+
/** Preferred popup side. Collision handling may flip it when space is limited. */
|
|
50
|
+
side?: ContentProps['side'];
|
|
51
|
+
sideOffset?: ContentProps['sideOffset'];
|
|
52
|
+
align?: ContentProps['align'];
|
|
53
|
+
alignOffset?: ContentProps['alignOffset'];
|
|
54
|
+
customAnchor?: ContentProps['customAnchor'];
|
|
55
|
+
strategy?: ContentProps['strategy'];
|
|
56
|
+
avoidCollisions?: ContentProps['avoidCollisions'];
|
|
57
|
+
collisionBoundary?: ContentProps['collisionBoundary'];
|
|
58
|
+
collisionPadding?: ContentProps['collisionPadding'];
|
|
59
|
+
sticky?: ContentProps['sticky'];
|
|
60
|
+
hideWhenDetached?: ContentProps['hideWhenDetached'];
|
|
61
|
+
updatePositionStrategy?: ContentProps['updatePositionStrategy'];
|
|
48
62
|
[key: string]: unknown;
|
|
49
63
|
}
|
|
50
64
|
|
|
@@ -73,6 +87,18 @@
|
|
|
73
87
|
renderValue,
|
|
74
88
|
onValueChange,
|
|
75
89
|
container,
|
|
90
|
+
side = 'bottom',
|
|
91
|
+
sideOffset = 4,
|
|
92
|
+
align = 'start',
|
|
93
|
+
alignOffset = 0,
|
|
94
|
+
customAnchor,
|
|
95
|
+
strategy,
|
|
96
|
+
avoidCollisions,
|
|
97
|
+
collisionBoundary,
|
|
98
|
+
collisionPadding,
|
|
99
|
+
sticky,
|
|
100
|
+
hideWhenDetached,
|
|
101
|
+
updatePositionStrategy,
|
|
76
102
|
...rest
|
|
77
103
|
}: Props = $props();
|
|
78
104
|
|
|
@@ -188,7 +214,7 @@
|
|
|
188
214
|
<SelectPrimitive.Trigger
|
|
189
215
|
child={trigger ? customTriggerChild : undefined}
|
|
190
216
|
class={cn(
|
|
191
|
-
'inline-flex w-full items-center justify-between gap-2 bg-kumo-
|
|
217
|
+
'inline-flex w-full items-center justify-between gap-2 bg-kumo-control text-left font-normal text-kumo-default shadow-xs outline-none ring ring-kumo-line transition focus:opacity-100 focus:ring-2 focus:ring-kumo-focus/50 focus-visible:ring-inset disabled:cursor-not-allowed disabled:bg-kumo-control/50 disabled:opacity-50 data-[state=open]:bg-kumo-control *:in-focus:opacity-100',
|
|
192
218
|
sizes[size],
|
|
193
219
|
!value || (Array.isArray(value) && value.length === 0) ? 'text-kumo-placeholder' : undefined,
|
|
194
220
|
errorMessage ? 'ring-kumo-danger! focus:ring-kumo-danger/50!' : undefined,
|
|
@@ -224,11 +250,22 @@
|
|
|
224
250
|
<SelectPrimitive.Portal to={container}>
|
|
225
251
|
<SelectPrimitive.Content
|
|
226
252
|
class={cn(
|
|
227
|
-
'z-50 flex max-h-[var(--bits-select-content-available-height)] min-w-[
|
|
253
|
+
'z-50 flex max-h-[var(--bits-select-content-available-height)] max-w-[var(--bits-select-content-available-width)] min-w-[var(--bits-select-anchor-width)] flex-col overflow-hidden rounded-lg bg-kumo-base py-1.5 text-base text-kumo-default shadow-lg ring ring-kumo-line outline-none',
|
|
228
254
|
contentClass
|
|
229
255
|
)}
|
|
230
256
|
preventScroll
|
|
231
|
-
|
|
257
|
+
{side}
|
|
258
|
+
{sideOffset}
|
|
259
|
+
{align}
|
|
260
|
+
{alignOffset}
|
|
261
|
+
{customAnchor}
|
|
262
|
+
{strategy}
|
|
263
|
+
{avoidCollisions}
|
|
264
|
+
{collisionBoundary}
|
|
265
|
+
{collisionPadding}
|
|
266
|
+
{sticky}
|
|
267
|
+
{hideWhenDetached}
|
|
268
|
+
{updatePositionStrategy}
|
|
232
269
|
>
|
|
233
270
|
<SelectPrimitive.Viewport class={cn('min-h-0 flex-1 overflow-y-auto overscroll-none scroll-pb-2 scroll-pt-2', viewportClass)}>
|
|
234
271
|
{#if children}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import { Select as SelectPrimitive } from 'bits-ui';
|
|
2
3
|
type Size = 'xs' | 'sm' | 'base' | 'lg';
|
|
3
4
|
type SelectValue = unknown;
|
|
4
5
|
type Value = SelectValue | SelectValue[];
|
|
@@ -12,6 +13,7 @@ type ItemDescriptor = string | {
|
|
|
12
13
|
disabled?: boolean;
|
|
13
14
|
};
|
|
14
15
|
type Items = Record<string, ItemDescriptor> | Option[];
|
|
16
|
+
type ContentProps = SelectPrimitive.ContentProps;
|
|
15
17
|
interface Props {
|
|
16
18
|
class?: string;
|
|
17
19
|
contentClass?: string;
|
|
@@ -42,6 +44,19 @@ interface Props {
|
|
|
42
44
|
renderValue?: (value: Value) => unknown;
|
|
43
45
|
onValueChange?: (value: Value) => void;
|
|
44
46
|
container?: HTMLElement | string;
|
|
47
|
+
/** Preferred popup side. Collision handling may flip it when space is limited. */
|
|
48
|
+
side?: ContentProps['side'];
|
|
49
|
+
sideOffset?: ContentProps['sideOffset'];
|
|
50
|
+
align?: ContentProps['align'];
|
|
51
|
+
alignOffset?: ContentProps['alignOffset'];
|
|
52
|
+
customAnchor?: ContentProps['customAnchor'];
|
|
53
|
+
strategy?: ContentProps['strategy'];
|
|
54
|
+
avoidCollisions?: ContentProps['avoidCollisions'];
|
|
55
|
+
collisionBoundary?: ContentProps['collisionBoundary'];
|
|
56
|
+
collisionPadding?: ContentProps['collisionPadding'];
|
|
57
|
+
sticky?: ContentProps['sticky'];
|
|
58
|
+
hideWhenDetached?: ContentProps['hideWhenDetached'];
|
|
59
|
+
updatePositionStrategy?: ContentProps['updatePositionStrategy'];
|
|
45
60
|
[key: string]: unknown;
|
|
46
61
|
}
|
|
47
62
|
declare const Select: import("svelte").Component<Props, {}, "value">;
|
|
@@ -27,7 +27,7 @@ export declare const KUMO_SELECT_STYLING: {
|
|
|
27
27
|
readonly height: 36;
|
|
28
28
|
readonly paddingX: 12;
|
|
29
29
|
readonly borderRadius: 8;
|
|
30
|
-
readonly background: "bg-kumo-
|
|
30
|
+
readonly background: "bg-kumo-control";
|
|
31
31
|
readonly text: "text-color-surface";
|
|
32
32
|
readonly ring: "color-border";
|
|
33
33
|
readonly fontSize: 16;
|
|
@@ -52,7 +52,7 @@ export declare const KUMO_SELECT_STYLING: {
|
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
54
|
readonly popup: {
|
|
55
|
-
readonly background: "bg-kumo-
|
|
55
|
+
readonly background: "bg-kumo-base";
|
|
56
56
|
readonly ring: "border-kumo-line";
|
|
57
57
|
readonly borderRadius: 8;
|
|
58
58
|
readonly padding: 6;
|
|
@@ -28,7 +28,7 @@ export const KUMO_SELECT_STYLING = {
|
|
|
28
28
|
height: 36,
|
|
29
29
|
paddingX: 12,
|
|
30
30
|
borderRadius: 8,
|
|
31
|
-
background: 'bg-kumo-
|
|
31
|
+
background: 'bg-kumo-control',
|
|
32
32
|
text: 'text-color-surface',
|
|
33
33
|
ring: 'color-border',
|
|
34
34
|
fontSize: 16,
|
|
@@ -43,7 +43,7 @@ export const KUMO_SELECT_STYLING = {
|
|
|
43
43
|
check: { name: 'ph-check', size: 20 }
|
|
44
44
|
},
|
|
45
45
|
popup: {
|
|
46
|
-
background: 'bg-kumo-
|
|
46
|
+
background: 'bg-kumo-base',
|
|
47
47
|
ring: 'border-kumo-line',
|
|
48
48
|
borderRadius: 8,
|
|
49
49
|
padding: 6
|
|
@@ -58,7 +58,7 @@ export const KUMO_SELECT_STYLING = {
|
|
|
58
58
|
};
|
|
59
59
|
export function selectVariants({ size = KUMO_SELECT_DEFAULT_VARIANTS.size } = {}) {
|
|
60
60
|
const buttonSize = KUMO_SELECT_VARIANTS.size[size].classes;
|
|
61
|
-
return cn('group flex w-full items-center select-none border-0 shadow-xs focus:outline-none focus:ring-kumo-focus/50 focus-visible:ring-2 focus-visible:ring-kumo-brand cursor-pointer disabled:cursor-not-allowed disabled:text-kumo-subtle', 'bg-kumo-
|
|
61
|
+
return cn('group flex w-full items-center select-none border-0 shadow-xs focus:outline-none focus:ring-kumo-focus/50 focus-visible:ring-2 focus-visible:ring-kumo-brand cursor-pointer disabled:cursor-not-allowed disabled:text-kumo-subtle', 'bg-kumo-control !text-kumo-default ring not-disabled:hover:bg-kumo-tint disabled:bg-kumo-control/50 disabled:!text-kumo-default/70 ring-kumo-line data-[state=open]:bg-kumo-control', buttonSize, 'justify-between font-normal', 'focus:opacity-100 focus:ring-kumo-focus/50 focus-visible:ring-inset *:in-focus:opacity-100');
|
|
62
62
|
}
|
|
63
63
|
export const triggerIconStyles = {
|
|
64
64
|
xs: { iconSize: 12, className: 'text-kumo-subtle' },
|