@kosmeo-ui/react 0.1.1 → 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.
Files changed (187) hide show
  1. package/dist/components/buttons/button/index.d.ts +2 -0
  2. package/dist/components/buttons/button/index.d.ts.map +1 -1
  3. package/dist/components/buttons/button/index.js +2 -2
  4. package/dist/components/buttons/button/index.js.map +1 -1
  5. package/dist/components/buttons/button/styles.css +6 -4
  6. package/dist/components/buttons/rowActions/styles.css +7 -0
  7. package/dist/components/data-display/addressBlock/styles.css +2 -4
  8. package/dist/components/data-display/dataTable/styles.css +15 -0
  9. package/dist/components/data-display/infiniteScroll/filters/searchFilter.d.ts.map +1 -1
  10. package/dist/components/data-display/infiniteScroll/filters/searchFilter.js +4 -35
  11. package/dist/components/data-display/infiniteScroll/filters/searchFilter.js.map +1 -1
  12. package/dist/components/data-display/infiniteScroll/filters/styles.css +4 -0
  13. package/dist/components/data-display/infiniteScroll/header/deleteButton.d.ts.map +1 -1
  14. package/dist/components/data-display/infiniteScroll/header/deleteButton.js +1 -1
  15. package/dist/components/data-display/infiniteScroll/header/deleteButton.js.map +1 -1
  16. package/dist/components/data-display/infiniteScroll/header/index.d.ts.map +1 -1
  17. package/dist/components/data-display/infiniteScroll/header/index.js +1 -1
  18. package/dist/components/data-display/infiniteScroll/header/index.js.map +1 -1
  19. package/dist/components/data-display/infiniteScroll/table/index.d.ts.map +1 -1
  20. package/dist/components/data-display/infiniteScroll/table/index.js +2 -2
  21. package/dist/components/data-display/infiniteScroll/table/index.js.map +1 -1
  22. package/dist/components/data-display/infiniteScroll/tags/index.d.ts.map +1 -1
  23. package/dist/components/data-display/infiniteScroll/tags/index.js +1 -6
  24. package/dist/components/data-display/infiniteScroll/tags/index.js.map +1 -1
  25. package/dist/components/data-display/infiniteScroll/tags/listFilterTag.d.ts.map +1 -1
  26. package/dist/components/data-display/infiniteScroll/tags/listFilterTag.js +3 -3
  27. package/dist/components/data-display/infiniteScroll/tags/listFilterTag.js.map +1 -1
  28. package/dist/components/data-display/infiniteScroll/tags/rangeFilterTag.d.ts.map +1 -1
  29. package/dist/components/data-display/infiniteScroll/tags/rangeFilterTag.js +3 -13
  30. package/dist/components/data-display/infiniteScroll/tags/rangeFilterTag.js.map +1 -1
  31. package/dist/components/data-display/infiniteScroll/tags/selectFilterTag.d.ts.map +1 -1
  32. package/dist/components/data-display/infiniteScroll/tags/selectFilterTag.js +3 -5
  33. package/dist/components/data-display/infiniteScroll/tags/selectFilterTag.js.map +1 -1
  34. package/dist/components/data-display/infiniteScroll/tags/styles.css +1 -29
  35. package/dist/components/data-display/recoveryCodeGrid/styles.css +1 -2
  36. package/dist/components/data-display/statCard/styles.css +1 -1
  37. package/dist/components/data-display/table/styles.css +12 -1
  38. package/dist/components/data-display/tagCard/index.d.ts.map +1 -1
  39. package/dist/components/data-display/tagCard/index.js +1 -1
  40. package/dist/components/data-display/tagCard/index.js.map +1 -1
  41. package/dist/components/feedback/badge/styles.css +8 -6
  42. package/dist/components/feedback/emptyState/styles.css +2 -4
  43. package/dist/components/feedback/progressBar/index.d.ts.map +1 -1
  44. package/dist/components/feedback/progressBar/index.js +7 -2
  45. package/dist/components/feedback/progressBar/index.js.map +1 -1
  46. package/dist/components/feedback/progressBar/styles.css +15 -6
  47. package/dist/components/feedback/spinner/styles.css +0 -13
  48. package/dist/components/forms/fieldShell/styles.css +1 -1
  49. package/dist/components/forms/formActions/styles.css +1 -1
  50. package/dist/components/forms/infoRow/styles.css +1 -2
  51. package/dist/components/forms/requiredAsterisk/styles.css +1 -1
  52. package/dist/components/index.d.ts +2 -0
  53. package/dist/components/index.d.ts.map +1 -1
  54. package/dist/components/index.js +1 -0
  55. package/dist/components/index.js.map +1 -1
  56. package/dist/components/inputs/checkbox/index.d.ts +6 -1
  57. package/dist/components/inputs/checkbox/index.d.ts.map +1 -1
  58. package/dist/components/inputs/checkbox/index.js +9 -2
  59. package/dist/components/inputs/checkbox/index.js.map +1 -1
  60. package/dist/components/inputs/checkbox/styles.css +8 -0
  61. package/dist/components/inputs/colorPicker/styles.css +1 -1
  62. package/dist/components/inputs/dateInput/index.d.ts +3 -3
  63. package/dist/components/inputs/dateInput/index.d.ts.map +1 -1
  64. package/dist/components/inputs/dateInput/index.js +13 -3
  65. package/dist/components/inputs/dateInput/index.js.map +1 -1
  66. package/dist/components/inputs/dropdown/index.d.ts +2 -0
  67. package/dist/components/inputs/dropdown/index.d.ts.map +1 -1
  68. package/dist/components/inputs/dropdown/index.js +1 -1
  69. package/dist/components/inputs/dropdown/index.js.map +1 -1
  70. package/dist/components/inputs/dropdownArrowButton/index.d.ts.map +1 -1
  71. package/dist/components/inputs/dropdownArrowButton/index.js +1 -1
  72. package/dist/components/inputs/dropdownArrowButton/index.js.map +1 -1
  73. package/dist/components/inputs/dropdownArrowButton/styles.css +1 -1
  74. package/dist/components/inputs/input/index.d.ts +5 -1
  75. package/dist/components/inputs/input/index.d.ts.map +1 -1
  76. package/dist/components/inputs/input/index.js +25 -4
  77. package/dist/components/inputs/input/index.js.map +1 -1
  78. package/dist/components/inputs/input/styles.css +46 -2
  79. package/dist/components/inputs/multiSelect/index.d.ts.map +1 -1
  80. package/dist/components/inputs/multiSelect/index.js +1 -1
  81. package/dist/components/inputs/multiSelect/index.js.map +1 -1
  82. package/dist/components/inputs/multiSelect/styles.css +1 -1
  83. package/dist/components/inputs/phoneInput/index.d.ts.map +1 -1
  84. package/dist/components/inputs/phoneInput/index.js +1 -1
  85. package/dist/components/inputs/phoneInput/index.js.map +1 -1
  86. package/dist/components/inputs/richTextEditor/index.d.ts.map +1 -1
  87. package/dist/components/inputs/richTextEditor/index.js +14 -3
  88. package/dist/components/inputs/richTextEditor/index.js.map +1 -1
  89. package/dist/components/layout/borderedSection/index.d.ts +3 -1
  90. package/dist/components/layout/borderedSection/index.d.ts.map +1 -1
  91. package/dist/components/layout/borderedSection/index.js +2 -2
  92. package/dist/components/layout/borderedSection/index.js.map +1 -1
  93. package/dist/components/layout/card/styles.css +22 -0
  94. package/dist/components/layout/sectionHeader/styles.css +1 -2
  95. package/dist/components/navigation/pagination/index.d.ts +23 -0
  96. package/dist/components/navigation/pagination/index.d.ts.map +1 -0
  97. package/dist/components/navigation/pagination/index.js +29 -0
  98. package/dist/components/navigation/pagination/index.js.map +1 -0
  99. package/dist/components/navigation/pagination/styles.css +113 -0
  100. package/dist/components/navigation/pagination/utils.d.ts +9 -0
  101. package/dist/components/navigation/pagination/utils.d.ts.map +1 -0
  102. package/dist/components/navigation/pagination/utils.js +25 -0
  103. package/dist/components/navigation/pagination/utils.js.map +1 -0
  104. package/dist/components/navigation/sidebar/styles.css +9 -1
  105. package/dist/components/navigation/tabButton/styles.css +6 -6
  106. package/dist/components/navigation/tabs/styles.css +2 -2
  107. package/dist/components/overlays/confirmationModal/index.d.ts +5 -0
  108. package/dist/components/overlays/confirmationModal/index.d.ts.map +1 -1
  109. package/dist/components/overlays/confirmationModal/index.js +23 -4
  110. package/dist/components/overlays/confirmationModal/index.js.map +1 -1
  111. package/dist/components/overlays/confirmationModal/styles.css +3 -0
  112. package/dist/components/overlays/modal/index.d.ts +2 -0
  113. package/dist/components/overlays/modal/index.d.ts.map +1 -1
  114. package/dist/components/overlays/modal/index.js +13 -4
  115. package/dist/components/overlays/modal/index.js.map +1 -1
  116. package/dist/hooks/index.d.ts +1 -0
  117. package/dist/hooks/index.d.ts.map +1 -1
  118. package/dist/hooks/index.js +1 -0
  119. package/dist/hooks/index.js.map +1 -1
  120. package/dist/hooks/usePaginated.d.ts +43 -0
  121. package/dist/hooks/usePaginated.d.ts.map +1 -0
  122. package/dist/hooks/usePaginated.js +60 -0
  123. package/dist/hooks/usePaginated.js.map +1 -0
  124. package/dist/styles.css +37 -14
  125. package/package.json +1 -1
  126. package/src/components/buttons/button/index.tsx +14 -5
  127. package/src/components/buttons/button/styles.css +6 -4
  128. package/src/components/buttons/rowActions/styles.css +7 -0
  129. package/src/components/data-display/addressBlock/styles.css +2 -4
  130. package/src/components/data-display/dataTable/styles.css +15 -0
  131. package/src/components/data-display/infiniteScroll/filters/searchFilter.tsx +16 -65
  132. package/src/components/data-display/infiniteScroll/filters/styles.css +4 -0
  133. package/src/components/data-display/infiniteScroll/header/deleteButton.tsx +1 -0
  134. package/src/components/data-display/infiniteScroll/header/index.tsx +4 -0
  135. package/src/components/data-display/infiniteScroll/table/index.tsx +2 -0
  136. package/src/components/data-display/infiniteScroll/tags/index.tsx +1 -11
  137. package/src/components/data-display/infiniteScroll/tags/listFilterTag.tsx +12 -16
  138. package/src/components/data-display/infiniteScroll/tags/rangeFilterTag.tsx +4 -22
  139. package/src/components/data-display/infiniteScroll/tags/selectFilterTag.tsx +4 -11
  140. package/src/components/data-display/infiniteScroll/tags/styles.css +1 -29
  141. package/src/components/data-display/recoveryCodeGrid/styles.css +1 -2
  142. package/src/components/data-display/statCard/styles.css +1 -1
  143. package/src/components/data-display/table/styles.css +12 -1
  144. package/src/components/data-display/tagCard/index.tsx +5 -3
  145. package/src/components/feedback/badge/styles.css +8 -6
  146. package/src/components/feedback/emptyState/styles.css +2 -4
  147. package/src/components/feedback/progressBar/index.tsx +14 -8
  148. package/src/components/feedback/progressBar/styles.css +15 -6
  149. package/src/components/feedback/spinner/styles.css +0 -13
  150. package/src/components/forms/fieldShell/styles.css +1 -1
  151. package/src/components/forms/formActions/styles.css +1 -1
  152. package/src/components/forms/infoRow/styles.css +1 -2
  153. package/src/components/forms/requiredAsterisk/styles.css +1 -1
  154. package/src/components/index.ts +2 -0
  155. package/src/components/inputs/checkbox/index.tsx +24 -4
  156. package/src/components/inputs/checkbox/styles.css +8 -0
  157. package/src/components/inputs/colorPicker/styles.css +1 -1
  158. package/src/components/inputs/dateInput/index.tsx +13 -4
  159. package/src/components/inputs/dropdown/index.tsx +4 -0
  160. package/src/components/inputs/dropdownArrowButton/index.tsx +1 -0
  161. package/src/components/inputs/dropdownArrowButton/styles.css +1 -1
  162. package/src/components/inputs/input/index.tsx +47 -4
  163. package/src/components/inputs/input/styles.css +46 -2
  164. package/src/components/inputs/multiSelect/index.tsx +1 -0
  165. package/src/components/inputs/multiSelect/styles.css +1 -1
  166. package/src/components/inputs/phoneInput/index.tsx +6 -2
  167. package/src/components/inputs/richTextEditor/index.tsx +27 -3
  168. package/src/components/layout/borderedSection/index.tsx +4 -1
  169. package/src/components/layout/card/styles.css +22 -0
  170. package/src/components/layout/sectionHeader/styles.css +1 -2
  171. package/src/components/navigation/pagination/index.tsx +134 -0
  172. package/src/components/navigation/pagination/styles.css +113 -0
  173. package/src/components/navigation/pagination/utils.ts +32 -0
  174. package/src/components/navigation/sidebar/styles.css +9 -1
  175. package/src/components/navigation/tabButton/styles.css +6 -6
  176. package/src/components/navigation/tabs/styles.css +2 -2
  177. package/src/components/overlays/confirmationModal/index.tsx +37 -1
  178. package/src/components/overlays/confirmationModal/styles.css +3 -0
  179. package/src/components/overlays/modal/index.tsx +23 -4
  180. package/src/hooks/index.ts +4 -0
  181. package/src/hooks/usePaginated.ts +123 -0
  182. package/src/styles.css +37 -14
  183. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.d.ts +0 -10
  184. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.d.ts.map +0 -1
  185. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.js +0 -22
  186. package/dist/components/data-display/infiniteScroll/tags/searchFilterTag.js.map +0 -1
  187. package/src/components/data-display/infiniteScroll/tags/searchFilterTag.tsx +0 -41
@@ -50,8 +50,12 @@ const RichTextEditor = (props: RichTextEditorProps) => {
50
50
  const [charCount, setCharCount] = useState(0);
51
51
  const [isInitialized, setIsInitialized] = useState(false);
52
52
 
53
- const fieldId =
54
- props.id || `richtext-${Math.random().toString(36).substring(2, 9)}`;
53
+ // Stable across renders: the editor reads labelId once, when it's created.
54
+ const [generatedId] = useState(
55
+ () => `richtext-${Math.random().toString(36).substring(2, 9)}`
56
+ );
57
+ const fieldId = props.id || generatedId;
58
+ const labelId = `${fieldId}-label`;
55
59
 
56
60
  const cleanHtml = (html: string): string | undefined => {
57
61
  // Remove trailing empty <p></p> tags
@@ -79,6 +83,14 @@ const RichTextEditor = (props: RichTextEditorProps) => {
79
83
  }),
80
84
  ],
81
85
  content: props.value,
86
+ editorProps: {
87
+ attributes: {
88
+ "aria-multiline": "true",
89
+ ...(props.label
90
+ ? { "aria-labelledby": labelId }
91
+ : { "aria-label": props.placeholder ?? "Rich text editor" }),
92
+ },
93
+ },
82
94
  editable: !disabled,
83
95
  autofocus: props.autofocus,
84
96
  onUpdate: ({ editor }) => {
@@ -146,7 +158,7 @@ const RichTextEditor = (props: RichTextEditorProps) => {
146
158
  <div className={`k-richtext-container k-richtext-container-${size}`}>
147
159
  {props.label && (
148
160
  <div className="k-richtext-label-wrapper">
149
- <label htmlFor={fieldId} className="k-richtext-label">
161
+ <label id={labelId} className="k-richtext-label">
150
162
  {props.label}
151
163
  {props.required && <MandatoryAsterisk />}
152
164
  </label>
@@ -165,6 +177,8 @@ const RichTextEditor = (props: RichTextEditorProps) => {
165
177
  <Tooltip content="Bold" position="top">
166
178
  <IconButton
167
179
  icon={faBold}
180
+ aria-label="Bold"
181
+ aria-pressed={editorState.isBold}
168
182
  size={size}
169
183
  variant={editorState.isBold ? "primary" : "secondary"}
170
184
  onClick={() => editor.chain().focus().toggleBold().run()}
@@ -174,6 +188,8 @@ const RichTextEditor = (props: RichTextEditorProps) => {
174
188
  <Tooltip content="Italic" position="top">
175
189
  <IconButton
176
190
  icon={faItalic}
191
+ aria-label="Italic"
192
+ aria-pressed={editorState.isItalic}
177
193
  size={size}
178
194
  variant={editorState.isItalic ? "primary" : "secondary"}
179
195
  onClick={() => editor.chain().focus().toggleItalic().run()}
@@ -183,6 +199,8 @@ const RichTextEditor = (props: RichTextEditorProps) => {
183
199
  <Tooltip content="Underline" position="top">
184
200
  <IconButton
185
201
  icon={faUnderline}
202
+ aria-label="Underline"
203
+ aria-pressed={editorState.isUnderlined}
186
204
  size={size}
187
205
  variant={editorState.isUnderlined ? "primary" : "secondary"}
188
206
  onClick={() => editor.chain().focus().toggleUnderline().run()}
@@ -193,6 +211,8 @@ const RichTextEditor = (props: RichTextEditorProps) => {
193
211
  <Tooltip content="Bullet List" position="top">
194
212
  <IconButton
195
213
  icon={faListUl}
214
+ aria-label="Bullet list"
215
+ aria-pressed={editorState.isBulletedList}
196
216
  size={size}
197
217
  variant={editorState.isBulletedList ? "primary" : "secondary"}
198
218
  onClick={() => editor.chain().focus().toggleBulletList().run()}
@@ -202,6 +222,8 @@ const RichTextEditor = (props: RichTextEditorProps) => {
202
222
  <Tooltip content="Numbered List" position="top">
203
223
  <IconButton
204
224
  icon={faListOl}
225
+ aria-label="Numbered list"
226
+ aria-pressed={editorState.isNumberedList}
205
227
  size={size}
206
228
  variant={editorState.isNumberedList ? "primary" : "secondary"}
207
229
  onClick={() => editor.chain().focus().toggleOrderedList().run()}
@@ -212,6 +234,7 @@ const RichTextEditor = (props: RichTextEditorProps) => {
212
234
  <Tooltip content="Clear Formatting" position="top">
213
235
  <IconButton
214
236
  icon={faTextSlash}
237
+ aria-label="Clear formatting"
215
238
  size={size}
216
239
  variant="secondary"
217
240
  onClick={() => {
@@ -234,6 +257,7 @@ const RichTextEditor = (props: RichTextEditorProps) => {
234
257
  >
235
258
  <IconButton
236
259
  icon={faCode}
260
+ aria-label={showHtml ? "Show editor" : "Show HTML"}
237
261
  size={size}
238
262
  variant={showHtml ? "primary" : "secondary"}
239
263
  onClick={() => setShowHtml(!showHtml)}
@@ -7,6 +7,8 @@ export interface BorderedSectionProps {
7
7
  children: ReactNode;
8
8
  /** Optional title shown above the body. */
9
9
  title?: ReactNode;
10
+ /** Heading element for the title, to fit the page's heading outline. Defaults to `h3`. Looks the same at every level. */
11
+ as?: "h2" | "h3" | "h4";
10
12
  /** Right-aligned actions in the title row. Implies a title row even when `title` is empty. */
11
13
  actions?: ReactNode;
12
14
  /** Body layout. `"stack"` (default) stacks children; `"row"` lays them out as a horizontal flex strip. */
@@ -20,6 +22,7 @@ export interface BorderedSectionProps {
20
22
  const BorderedSection = ({
21
23
  children,
22
24
  title,
25
+ as: Heading = "h3",
23
26
  actions,
24
27
  layout = "stack",
25
28
  gap,
@@ -40,7 +43,7 @@ const BorderedSection = ({
40
43
  {showHeader && (
41
44
  <div className="k-bordered-section-header">
42
45
  {title !== undefined && (
43
- <h3 className="k-bordered-section-title">{title}</h3>
46
+ <Heading className="k-bordered-section-title">{title}</Heading>
44
47
  )}
45
48
  {actions !== undefined && (
46
49
  <div className="k-bordered-section-actions">{actions}</div>
@@ -76,8 +76,30 @@
76
76
  }
77
77
 
78
78
  /* Padding Variants */
79
+ /* Edge-to-edge content (tables, images): clip it to the rounded corners, and
80
+ give the header and footer their own padding instead. */
79
81
  .k-card-padding-none {
80
82
  padding: 0;
83
+ overflow: hidden;
84
+ }
85
+
86
+ .k-card-padding-none .k-card-header {
87
+ padding: 0.75rem 1rem;
88
+ margin-bottom: 0;
89
+ }
90
+
91
+ .k-card-padding-none .k-card-footer {
92
+ padding: 0.75rem 1rem;
93
+ margin-top: 0;
94
+ }
95
+
96
+ /* The card already draws the outer edge and the line under the header. */
97
+ .k-card-padding-none .k-card-body > .k-table {
98
+ border: none;
99
+ }
100
+
101
+ .k-card-padding-none .k-card-body > .k-table thead th {
102
+ border-top: none;
81
103
  }
82
104
 
83
105
  .k-card-padding-small {
@@ -82,8 +82,7 @@
82
82
 
83
83
  .k-section-header-subtitle {
84
84
  font-size: 0.8125rem;
85
- color: var(--text-color);
86
- opacity: 0.65;
85
+ color: var(--text-color-secondary);
87
86
  }
88
87
 
89
88
  .k-section-header-badges {
@@ -0,0 +1,134 @@
1
+ "use client";
2
+
3
+ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4
+ import Select from "../../inputs/select/index.js";
5
+ import { PropsWithSize } from "../../types.js";
6
+ import { pageRange } from "./utils.js";
7
+ import { useId } from "react";
8
+ import "./styles.css";
9
+ import {
10
+ faChevronLeft,
11
+ faChevronRight,
12
+ } from "@fortawesome/free-solid-svg-icons";
13
+
14
+ export interface PaginationProps extends PropsWithSize {
15
+ /** Current page, starting at 1. */
16
+ page: number;
17
+ /** Total number of pages. */
18
+ pageCount: number;
19
+ onPageChange: (page: number) => void;
20
+ /** Total item count. With `pageSize`, shows a "51–75 of 240" summary. */
21
+ total?: number;
22
+ /** Items per page. Needed for the summary and the page-size picker. */
23
+ pageSize?: number;
24
+ /** Page sizes to offer. With `onPageSizeChange`, shows a "Rows per page" picker. */
25
+ pageSizeOptions?: number[];
26
+ onPageSizeChange?: (pageSize: number) => void;
27
+ /** Pages shown either side of the current page before a "…" gap. Defaults to 1. */
28
+ siblingCount?: number;
29
+ /** Accessible name for the navigation landmark. Defaults to "Pagination". */
30
+ "aria-label"?: string;
31
+ }
32
+
33
+ const Pagination = ({
34
+ page,
35
+ pageCount,
36
+ onPageChange,
37
+ total,
38
+ pageSize,
39
+ pageSizeOptions,
40
+ onPageSizeChange,
41
+ siblingCount = 1,
42
+ size = "md",
43
+ "aria-label": ariaLabel = "Pagination",
44
+ }: PaginationProps) => {
45
+ const pageSizeId = useId();
46
+ const lastPage = Math.max(pageCount, 1);
47
+ const current = Math.min(Math.max(page, 1), lastPage);
48
+
49
+ const goTo = (target: number) => {
50
+ if (target >= 1 && target <= lastPage && target !== current) {
51
+ onPageChange(target);
52
+ }
53
+ };
54
+
55
+ const summary =
56
+ total !== undefined && pageSize !== undefined
57
+ ? total === 0
58
+ ? "No results"
59
+ : `${(current - 1) * pageSize + 1}–${Math.min(current * pageSize, total)} of ${total.toLocaleString()}`
60
+ : undefined;
61
+
62
+ return (
63
+ <nav className={`k-pagination k-pagination-${size}`} aria-label={ariaLabel}>
64
+ {summary && (
65
+ <p className="k-pagination-summary" aria-live="polite">
66
+ {summary}
67
+ </p>
68
+ )}
69
+
70
+ <ul className="k-pagination-pages">
71
+ <li>
72
+ <button
73
+ type="button"
74
+ className="k-pagination-button"
75
+ onClick={() => goTo(current - 1)}
76
+ disabled={current <= 1}
77
+ aria-label="Previous page"
78
+ >
79
+ <FontAwesomeIcon icon={faChevronLeft} aria-hidden="true" />
80
+ </button>
81
+ </li>
82
+ {pageRange(current, lastPage, siblingCount).map((item) =>
83
+ typeof item === "number" ? (
84
+ <li key={item}>
85
+ <button
86
+ type="button"
87
+ className={`k-pagination-button${item === current ? " k-pagination-button-current" : ""}`}
88
+ onClick={() => goTo(item)}
89
+ aria-label={`Page ${item}`}
90
+ aria-current={item === current ? "page" : undefined}
91
+ >
92
+ {item}
93
+ </button>
94
+ </li>
95
+ ) : (
96
+ <li key={item} className="k-pagination-gap" aria-hidden="true">
97
+ …
98
+ </li>
99
+ )
100
+ )}
101
+ <li>
102
+ <button
103
+ type="button"
104
+ className="k-pagination-button"
105
+ onClick={() => goTo(current + 1)}
106
+ disabled={current >= lastPage}
107
+ aria-label="Next page"
108
+ >
109
+ <FontAwesomeIcon icon={faChevronRight} aria-hidden="true" />
110
+ </button>
111
+ </li>
112
+ </ul>
113
+
114
+ {pageSizeOptions && onPageSizeChange && (
115
+ <div className="k-pagination-page-size">
116
+ <label htmlFor={pageSizeId}>Rows per page</label>
117
+ {/* Select works in strings. */}
118
+ <Select
119
+ id={pageSizeId}
120
+ size="sm"
121
+ value={pageSize === undefined ? undefined : String(pageSize)}
122
+ options={pageSizeOptions.map((n) => ({
123
+ value: String(n),
124
+ label: String(n),
125
+ }))}
126
+ onChange={(n) => n !== undefined && onPageSizeChange(Number(n))}
127
+ />
128
+ </div>
129
+ )}
130
+ </nav>
131
+ );
132
+ };
133
+
134
+ export default Pagination;
@@ -0,0 +1,113 @@
1
+ .k-pagination {
2
+ display: flex;
3
+ flex-wrap: wrap;
4
+ align-items: center;
5
+ justify-content: space-between;
6
+ gap: 0.75rem 1.5rem;
7
+ color: var(--text-color);
8
+ }
9
+
10
+ .k-pagination-summary {
11
+ margin: 0;
12
+ color: var(--text-color-secondary);
13
+ font-variant-numeric: tabular-nums;
14
+ }
15
+
16
+ .k-pagination-pages {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 0.25rem;
20
+ margin: 0;
21
+ padding: 0;
22
+ list-style: none;
23
+ }
24
+
25
+ .k-pagination-button {
26
+ display: inline-flex;
27
+ align-items: center;
28
+ justify-content: center;
29
+ border: 0.0625rem solid var(--border-color);
30
+ border-radius: 0.375rem;
31
+ background-color: var(--background-color-light);
32
+ color: var(--text-color);
33
+ font-variant-numeric: tabular-nums;
34
+ cursor: pointer;
35
+ transition:
36
+ background-color 0.15s ease-in-out,
37
+ border-color 0.15s ease-in-out;
38
+ }
39
+
40
+ .k-pagination-button:not(:disabled):hover {
41
+ background-color: var(--background-color-hover);
42
+ }
43
+
44
+ .k-pagination-button:focus-visible {
45
+ outline: 0.125rem solid var(--border-color-active);
46
+ outline-offset: 0.125rem;
47
+ }
48
+
49
+ .k-pagination-button-current,
50
+ .k-pagination-button-current:not(:disabled):hover {
51
+ border-color: var(--primary-color);
52
+ background-color: var(--primary-color);
53
+ color: var(--text-color-light);
54
+ font-weight: 600;
55
+ }
56
+
57
+ .k-pagination-button:disabled {
58
+ cursor: not-allowed;
59
+ opacity: 0.5;
60
+ }
61
+
62
+ .k-pagination-gap {
63
+ color: var(--text-color-secondary);
64
+ text-align: center;
65
+ }
66
+
67
+ .k-pagination-page-size {
68
+ display: flex;
69
+ align-items: center;
70
+ gap: 0.5rem;
71
+ color: var(--text-color-secondary);
72
+ white-space: nowrap;
73
+ }
74
+
75
+ /* Sizes: every button stays at least 32px, above the 24px target minimum. */
76
+ .k-pagination-sm {
77
+ font-size: var(--text-size-sm);
78
+ }
79
+
80
+ .k-pagination-sm .k-pagination-button,
81
+ .k-pagination-sm .k-pagination-gap {
82
+ min-width: 2rem;
83
+ height: 2rem;
84
+ padding: 0 0.5rem;
85
+ }
86
+
87
+ .k-pagination-md {
88
+ font-size: var(--text-size-sm);
89
+ }
90
+
91
+ .k-pagination-md .k-pagination-button,
92
+ .k-pagination-md .k-pagination-gap {
93
+ min-width: 2.25rem;
94
+ height: 2.25rem;
95
+ padding: 0 0.625rem;
96
+ }
97
+
98
+ .k-pagination-lg {
99
+ font-size: var(--text-size-md);
100
+ }
101
+
102
+ .k-pagination-lg .k-pagination-button,
103
+ .k-pagination-lg .k-pagination-gap {
104
+ min-width: 2.75rem;
105
+ height: 2.75rem;
106
+ padding: 0 0.75rem;
107
+ }
108
+
109
+ .k-pagination-gap {
110
+ display: inline-flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ }
@@ -0,0 +1,32 @@
1
+ export type PageRangeItem = number | "start-gap" | "end-gap";
2
+
3
+ /**
4
+ * The page buttons to show: always the first and last page, the current page
5
+ * with `siblings` neighbours either side, and a gap marker wherever pages are
6
+ * skipped. The list is the same length on every page, so the control doesn't
7
+ * jump around as you move through it.
8
+ */
9
+ export const pageRange = (
10
+ page: number,
11
+ pageCount: number,
12
+ siblings = 1
13
+ ): PageRangeItem[] => {
14
+ const range = (from: number, to: number) =>
15
+ Array.from({ length: to - from + 1 }, (_, i) => from + i);
16
+
17
+ // first + last + current + siblings + two gaps
18
+ const slots = siblings * 2 + 5;
19
+ if (pageCount <= slots) return range(1, pageCount);
20
+
21
+ const left = Math.max(page - siblings, 1);
22
+ const right = Math.min(page + siblings, pageCount);
23
+ const gapBefore = left > 2;
24
+ const gapAfter = right < pageCount - 1;
25
+ // Pages shown at the start or end when only one gap is needed.
26
+ const edge = siblings * 2 + 3;
27
+
28
+ if (!gapBefore) return [...range(1, edge), "end-gap", pageCount];
29
+ if (!gapAfter)
30
+ return [1, "start-gap", ...range(pageCount - edge + 1, pageCount)];
31
+ return [1, "start-gap", ...range(left, right), "end-gap", pageCount];
32
+ };
@@ -14,7 +14,9 @@
14
14
  font-weight: 700;
15
15
  text-transform: uppercase;
16
16
  letter-spacing: 0.05em;
17
- color: var(--overlay-light-medium);
17
+ /* 50% white: the weakest overlay that keeps small text above 4.5:1 on the
18
+ sidebar's dark background. */
19
+ color: var(--overlay-light-strong);
18
20
  white-space: nowrap;
19
21
  overflow: hidden;
20
22
  }
@@ -38,6 +40,12 @@
38
40
  background-color: var(--overlay-light-weak);
39
41
  }
40
42
 
43
+ /* Browser default focus rings can vanish on the dark background. */
44
+ .k-sidebar-nav-link:focus-visible {
45
+ outline: 2px solid var(--text-color-light);
46
+ outline-offset: -2px;
47
+ }
48
+
41
49
  .k-sidebar-nav-link.active {
42
50
  background-color: var(--overlay-light-medium);
43
51
  color: var(--text-color-light);
@@ -35,12 +35,12 @@
35
35
  }
36
36
 
37
37
  .k-tab-button-primary:hover:not(.k-tab-button-disabled) {
38
- color: var(--primary-color);
38
+ color: var(--primary-color-text);
39
39
  background-color: var(--background-color);
40
40
  }
41
41
 
42
42
  .k-tab-button-primary.k-tab-button-active {
43
- color: var(--primary-color);
43
+ color: var(--primary-color-text);
44
44
  background-color: var(--background-color);
45
45
  border-bottom-color: var(--primary-color);
46
46
  border-color: var(--primary-color) var(--primary-color) transparent
@@ -89,12 +89,12 @@
89
89
  }
90
90
 
91
91
  .k-tab-button-danger:hover:not(.k-tab-button-disabled) {
92
- color: var(--danger-color);
92
+ color: var(--danger-color-text);
93
93
  background-color: var(--background-color);
94
94
  }
95
95
 
96
96
  .k-tab-button-danger.k-tab-button-active {
97
- color: var(--danger-color);
97
+ color: var(--danger-color-text);
98
98
  background-color: var(--background-color);
99
99
  border-bottom-color: var(--danger-color);
100
100
  border-color: var(--danger-color) var(--danger-color) transparent
@@ -107,12 +107,12 @@
107
107
  }
108
108
 
109
109
  .k-tab-button-warning:hover:not(.k-tab-button-disabled) {
110
- color: var(--warning-color);
110
+ color: var(--warning-color-text);
111
111
  background-color: var(--background-color);
112
112
  }
113
113
 
114
114
  .k-tab-button-warning.k-tab-button-active {
115
- color: var(--warning-color);
115
+ color: var(--warning-color-text);
116
116
  background-color: var(--background-color);
117
117
  border-bottom-color: var(--warning-color);
118
118
  border-color: var(--warning-color) var(--warning-color) transparent
@@ -28,11 +28,11 @@
28
28
  }
29
29
 
30
30
  .k-tabs-button:hover:not(:disabled) {
31
- color: var(--primary-color);
31
+ color: var(--primary-color-text);
32
32
  }
33
33
 
34
34
  .k-tabs-button-active {
35
- color: var(--primary-color);
35
+ color: var(--primary-color-text);
36
36
  border-bottom-color: var(--primary-color);
37
37
  }
38
38
 
@@ -1,7 +1,10 @@
1
1
  "use client";
2
2
 
3
3
  import Button from "../../buttons/button/index.js";
4
+ import Input from "../../inputs/input/index.js";
4
5
  import Modal from "../modal/index.js";
6
+ import { useState } from "react";
7
+ import "./styles.css";
5
8
 
6
9
  export interface ConfirmationModalProps {
7
10
  show: boolean;
@@ -12,14 +15,34 @@ export interface ConfirmationModalProps {
12
15
  confirmButtonText: string;
13
16
  cancelButtonText?: string;
14
17
  isLoading?: boolean;
18
+ /** **_Optional_** text the user must type, exactly, before the confirm button enables (e.g. the name of what's being deleted) */
19
+ confirmationText?: string;
20
+ /** **_Optional_** whether Escape and a click outside close the modal. Defaults to `true` */
21
+ dismissible?: boolean;
15
22
  }
16
23
 
17
24
  const ConfirmationModal = (props: ConfirmationModalProps) => {
25
+ const [typed, setTyped] = useState("");
26
+ const [wasShown, setWasShown] = useState(props.show);
27
+
28
+ // Clear the typed text each time the modal opens, so a match from a
29
+ // previous confirmation never carries over.
30
+ if (props.show !== wasShown) {
31
+ setWasShown(props.show);
32
+ if (props.show) setTyped("");
33
+ }
34
+
35
+ const confirmed =
36
+ props.confirmationText === undefined || typed === props.confirmationText;
37
+ const canConfirm = confirmed && !props.isLoading;
38
+
18
39
  return (
19
40
  <Modal
20
41
  show={props.show}
21
42
  title={props.title}
22
43
  onClose={props.handleHide}
44
+ // Cancel is disabled while loading; Escape and the backdrop follow suit.
45
+ dismissible={props.dismissible !== false && !props.isLoading}
23
46
  footerButtons={[
24
47
  <Button
25
48
  key="cancel"
@@ -34,13 +57,26 @@ const ConfirmationModal = (props: ConfirmationModalProps) => {
34
57
  variant="danger"
35
58
  onClick={props.handleConfirm}
36
59
  isLoading={props.isLoading}
37
- disabled={props.isLoading}
60
+ disabled={!canConfirm}
38
61
  >
39
62
  {props.confirmButtonText}
40
63
  </Button>,
41
64
  ]}
42
65
  >
43
66
  {props.body}
67
+ {props.confirmationText !== undefined && (
68
+ <div className="k-confirmation-modal-check">
69
+ <Input
70
+ label={`Type "${props.confirmationText}" to confirm`}
71
+ value={typed}
72
+ onChange={(e) => setTyped(e.target.value)}
73
+ onKeyDown={(e) => {
74
+ if (e.key === "Enter" && canConfirm) props.handleConfirm();
75
+ }}
76
+ autoComplete="off"
77
+ />
78
+ </div>
79
+ )}
44
80
  </Modal>
45
81
  );
46
82
  };
@@ -0,0 +1,3 @@
1
+ .k-confirmation-modal-check {
2
+ margin-top: 1rem;
3
+ }
@@ -23,13 +23,16 @@ interface ModalProps extends PropsWithChildren {
23
23
  * editors) that would feel cramped at `lg`.
24
24
  */
25
25
  size?: ModalSize;
26
+ /** **_Optional_** whether Escape and a click on the backdrop close the modal. When `false`, only the modal's own buttons can close it. Defaults to `true` */
27
+ dismissible?: boolean;
26
28
  }
27
29
 
28
30
  const Modal = (props: ModalProps) => {
29
- const { size = "md" } = props;
31
+ const { size = "md", dismissible = true } = props;
30
32
  const [shouldRender, setShouldRender] = useState(props.show);
31
33
  const [isAnimating, setIsAnimating] = useState(false);
32
34
  const contentRef = useRef<HTMLDivElement>(null);
35
+ const pressStartedOnOverlay = useRef(false);
33
36
 
34
37
  useEffect(() => {
35
38
  if (props.show) {
@@ -46,13 +49,13 @@ const Modal = (props: ModalProps) => {
46
49
  }, [props.show]);
47
50
 
48
51
  useEffect(() => {
49
- if (!props.show) return;
52
+ if (!props.show || !dismissible) return;
50
53
  const handler = (e: KeyboardEvent) => {
51
54
  if (e.key === "Escape") props.onClose();
52
55
  };
53
56
  document.addEventListener("keydown", handler);
54
57
  return () => document.removeEventListener("keydown", handler);
55
- }, [props.show, props.onClose]);
58
+ }, [props.show, props.onClose, dismissible]);
56
59
 
57
60
  useFocusTrap(contentRef, !!props.show && !!shouldRender);
58
61
 
@@ -60,7 +63,23 @@ const Modal = (props: ModalProps) => {
60
63
 
61
64
  return (
62
65
  <Portal>
63
- <div className={`k-modal-overlay ${isAnimating ? "closing" : ""}`}>
66
+ {/* Close only when the press starts and ends on the backdrop, so a text
67
+ selection dragged out of the dialog doesn't close it. */}
68
+ <div
69
+ className={`k-modal-overlay ${isAnimating ? "closing" : ""}`}
70
+ onMouseDown={(e) => {
71
+ pressStartedOnOverlay.current = e.target === e.currentTarget;
72
+ }}
73
+ onClick={(e) => {
74
+ if (
75
+ dismissible &&
76
+ pressStartedOnOverlay.current &&
77
+ e.target === e.currentTarget
78
+ ) {
79
+ props.onClose();
80
+ }
81
+ }}
82
+ >
64
83
  <div
65
84
  ref={contentRef}
66
85
  className={`k-modal-content ${isAnimating ? "closing" : ""} ${size}`}
@@ -4,6 +4,10 @@ export {
4
4
  type UseInfiniteReturn,
5
5
  } from "./useInfinite.js";
6
6
  export { default as useInfiniteCursor } from "./useInfiniteCursor.js";
7
+ export {
8
+ default as usePaginated,
9
+ type UsePaginatedReturn,
10
+ } from "./usePaginated.js";
7
11
  export { default as useScrollIntoView } from "./useScrollIntoView.js";
8
12
  export { default as useDebouncedValue } from "./useDebouncedValue.js";
9
13
  export { default as useOutsideClick } from "./useOutsideClick.js";