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.
Files changed (94) hide show
  1. package/ai/component-registry.json +112 -2
  2. package/ai/component-registry.md +1 -1
  3. package/dist/components/combobox/ComboboxInput.svelte +1 -1
  4. package/dist/components/combobox/ComboboxTest.svelte +2 -0
  5. package/dist/components/combobox/combobox.test.js +9 -0
  6. package/dist/components/dialog/Dialog.svelte +3 -7
  7. package/dist/components/dialog/Dialog.svelte.d.ts +1 -1
  8. package/dist/components/dialog/Dialog.variants.d.ts +4 -0
  9. package/dist/components/dialog/Dialog.variants.js +4 -0
  10. package/dist/components/dialog/dialog.test.js +8 -1
  11. package/dist/components/dialog/index.d.ts +2 -1
  12. package/dist/components/dialog/index.js +1 -1
  13. package/dist/components/input-group/context.js +1 -1
  14. package/dist/components/input-group/input-group.test.js +4 -0
  15. package/dist/components/pagination/Pagination.svelte +6 -0
  16. package/dist/components/pagination/Pagination.svelte.d.ts +2 -0
  17. package/dist/components/pagination/PaginationControls.svelte +13 -5
  18. package/dist/components/pagination/context.d.ts +1 -0
  19. package/dist/components/pagination/pagination.test.js +19 -0
  20. package/dist/components/select/Select.svelte +40 -3
  21. package/dist/components/select/Select.svelte.d.ts +15 -0
  22. package/dist/components/select/select-variants.d.ts +2 -2
  23. package/dist/components/select/select-variants.js +3 -3
  24. package/dist/components/select/select.test.js +103 -81
  25. package/dist/components/table/Table.svelte +1 -1
  26. package/dist/components/table/Table.svelte.d.ts +1 -1
  27. package/dist/components/table/table.test.js +5 -0
  28. package/dist/components/text/Text.svelte +4 -4
  29. package/dist/components/text/Text.svelte.d.ts +5 -5
  30. package/dist/components/text/text.test.js +12 -1
  31. package/dist/components/toolbar/ToolbarLink.svelte +48 -0
  32. package/dist/components/toolbar/ToolbarLink.svelte.d.ts +20 -0
  33. package/dist/components/toolbar/ToolbarTestHost.svelte +6 -0
  34. package/dist/components/toolbar/index.d.ts +4 -1
  35. package/dist/components/toolbar/index.js +3 -1
  36. package/dist/components/toolbar/toolbar.test.js +15 -1
  37. package/dist/components/tooltip/Tooltip.svelte +1 -1
  38. package/dist/components/tooltip/TooltipTestHost.svelte +4 -0
  39. package/dist/components/tooltip/tooltip.test.js +6 -0
  40. package/dist/docs/demo-snippets/PaginationUnknownTotalDemo.svelte +12 -0
  41. package/dist/docs/demo-snippets/PaginationUnknownTotalDemo.svelte.d.ts +3 -0
  42. package/dist/docs/demo-snippets/SelectPlacementDemo.svelte +20 -0
  43. package/dist/docs/demo-snippets/SelectPlacementDemo.svelte.d.ts +18 -0
  44. package/dist/docs/demo-snippets/TabsDefaultDemo.svelte +1 -1
  45. package/dist/docs/demo-snippets/TabsSmDemo.svelte +1 -1
  46. package/dist/docs/demo-snippets/ToolbarLinksDemo.svelte +10 -0
  47. package/dist/docs/demo-snippets/ToolbarLinksDemo.svelte.d.ts +18 -0
  48. package/dist/docs/props-data/Pagination.js +6 -0
  49. package/dist/docs/props-data/Select.js +52 -0
  50. package/dist/docs/props-data/Toolbar__Link.d.ts +6 -0
  51. package/dist/docs/props-data/Toolbar__Link.js +32 -0
  52. package/dist/index.d.ts +2 -2
  53. package/dist/index.js +1 -1
  54. package/dist/styles.css +1 -1
  55. package/package.json +1 -1
  56. package/src/lib/components/combobox/ComboboxInput.svelte +1 -1
  57. package/src/lib/components/combobox/ComboboxTest.svelte +2 -0
  58. package/src/lib/components/combobox/combobox.test.ts +11 -0
  59. package/src/lib/components/dialog/Dialog.svelte +3 -7
  60. package/src/lib/components/dialog/Dialog.variants.ts +12 -0
  61. package/src/lib/components/dialog/dialog.test.ts +9 -1
  62. package/src/lib/components/dialog/index.ts +2 -1
  63. package/src/lib/components/input-group/context.ts +1 -1
  64. package/src/lib/components/input-group/input-group.test.ts +5 -0
  65. package/src/lib/components/pagination/Pagination.svelte +6 -0
  66. package/src/lib/components/pagination/PaginationControls.svelte +13 -5
  67. package/src/lib/components/pagination/context.ts +1 -0
  68. package/src/lib/components/pagination/pagination.test.ts +23 -0
  69. package/src/lib/components/select/Select.svelte +40 -3
  70. package/src/lib/components/select/select-variants.ts +3 -3
  71. package/src/lib/components/select/select.test.ts +111 -81
  72. package/src/lib/components/table/Table.svelte +1 -1
  73. package/src/lib/components/table/table.test.ts +12 -0
  74. package/src/lib/components/text/Text.svelte +4 -4
  75. package/src/lib/components/text/text.test.ts +13 -1
  76. package/src/lib/components/toolbar/ToolbarLink.svelte +48 -0
  77. package/src/lib/components/toolbar/ToolbarTestHost.svelte +6 -0
  78. package/src/lib/components/toolbar/index.ts +5 -0
  79. package/src/lib/components/toolbar/toolbar.test.ts +17 -0
  80. package/src/lib/components/tooltip/Tooltip.svelte +1 -1
  81. package/src/lib/components/tooltip/TooltipTestHost.svelte +4 -0
  82. package/src/lib/components/tooltip/tooltip.test.ts +7 -0
  83. package/src/lib/docs/demo-snippets/PaginationUnknownTotalDemo.svelte +12 -0
  84. package/src/lib/docs/demo-snippets/SelectPlacementDemo.svelte +20 -0
  85. package/src/lib/docs/demo-snippets/TabsDefaultDemo.svelte +1 -1
  86. package/src/lib/docs/demo-snippets/TabsSmDemo.svelte +1 -1
  87. package/src/lib/docs/demo-snippets/ToolbarLinksDemo.svelte +10 -0
  88. package/src/lib/docs/props-data/Pagination.ts +6 -0
  89. package/src/lib/docs/props-data/Select.ts +52 -0
  90. package/src/lib/docs/props-data/Toolbar__Link.ts +32 -0
  91. package/src/lib/index.ts +2 -0
  92. package/src/routes/components/pagination/+page.md +17 -0
  93. package/src/routes/components/select/+page.md +41 -63
  94. package/src/routes/components/toolbar/+page.md +13 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.12.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-elevated",
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
  },
@@ -1,6 +1,6 @@
1
1
  # Kumo Svelte Component Registry
2
2
 
3
- Version: 0.12.0
3
+ Version: 0.13.0
4
4
 
5
5
  ## Autocomplete
6
6
 
@@ -13,7 +13,7 @@
13
13
  </script>
14
14
 
15
15
  <input
16
- class={cn(inputStyles[context.size], 'mx-1.5 w-[calc(100%-0.75rem)] shrink-0 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)}
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-1.5',
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 = clamp(nextPage, 1, context.maxPage);
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 controls === 'full'}
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 controls === 'full'}
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={context.page === context.maxPage}
106
+ disabled={isNextDisabled}
99
107
  onclick={() => commitPage(context.page + 1)}
100
108
  >
101
109
  <CaretRight class="size-4" />
102
110
  </InputGroupButton>
103
- {#if controls === 'full'}
111
+ {#if showFullControls}
104
112
  <InputGroupButton
105
113
  variant="secondary"
106
114
  aria-label={context.labels.lastPage}
@@ -12,6 +12,7 @@ export interface PaginationContextValue {
12
12
  page: number;
13
13
  perPage?: number;
14
14
  totalCount?: number;
15
+ hasNextPage?: boolean;
15
16
  maxPage: number;
16
17
  pageShowingRange: string;
17
18
  setPage: (page: number) => void;
@@ -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-base 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:opacity-50 *:in-focus:opacity-100',
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-[calc(var(--bits-select-anchor-width)+3px)] 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',
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
- sideOffset={4}
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-elevated";
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-elevated";
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-elevated',
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-elevated',
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-base !text-kumo-default ring not-disabled:hover:bg-kumo-tint disabled:bg-kumo-base/50 disabled:!text-kumo-default/70 ring-kumo-line data-[state=open]:bg-kumo-base', buttonSize, 'justify-between font-normal', 'focus:opacity-100 focus:ring-kumo-focus/50 focus-visible:ring-inset *:in-focus:opacity-100');
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' },