@rotki/ui-library 3.0.0-beta.5 → 3.1.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 (163) hide show
  1. package/dist/components/accordions/accordion/RuiAccordion.js.map +1 -1
  2. package/dist/components/accordions/accordion/RuiAccordion.vue_vue_type_script_setup_true_lang.js +13 -2
  3. package/dist/components/accordions/accordion/RuiAccordion.vue_vue_type_script_setup_true_lang.js.map +1 -1
  4. package/dist/components/alerts/RuiAlert.vue.d.ts +2 -2
  5. package/dist/components/buttons/button/RuiButton.js.map +1 -1
  6. package/dist/components/buttons/button/RuiButton.vue.d.ts +7 -1
  7. package/dist/components/buttons/button/RuiButton.vue_vue_type_script_setup_true_lang.js +4 -2
  8. package/dist/components/buttons/button/RuiButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  9. package/dist/components/buttons/button/button-props.d.ts +3 -0
  10. package/dist/components/buttons/button/button-props.js +2 -1
  11. package/dist/components/buttons/button/button-props.js.map +1 -1
  12. package/dist/components/buttons/button/button-styles.d.ts +75 -0
  13. package/dist/components/buttons/button/button-styles.js +135 -4
  14. package/dist/components/buttons/button/button-styles.js.map +1 -1
  15. package/dist/components/cards/RuiCard.js.map +1 -1
  16. package/dist/components/cards/RuiCard.vue.d.ts +6 -0
  17. package/dist/components/cards/RuiCard.vue_vue_type_script_setup_true_lang.js +14 -2
  18. package/dist/components/cards/RuiCard.vue_vue_type_script_setup_true_lang.js.map +1 -1
  19. package/dist/components/chips/RuiChip.js.map +1 -1
  20. package/dist/components/chips/RuiChip.vue_vue_type_script_setup_true_lang.js +14 -0
  21. package/dist/components/chips/RuiChip.vue_vue_type_script_setup_true_lang.js.map +1 -1
  22. package/dist/components/chips/chip-props.d.ts +2 -0
  23. package/dist/components/chips/chip-props.js +3 -0
  24. package/dist/components/chips/chip-props.js.map +1 -1
  25. package/dist/components/color-picker/RuiColorInput.js.map +1 -1
  26. package/dist/components/color-picker/RuiColorInput.vue_vue_type_script_setup_true_lang.js +2 -2
  27. package/dist/components/color-picker/RuiColorInput.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/date-time-picker/RuiDateTimePicker.js.map +1 -1
  29. package/dist/components/date-time-picker/RuiDateTimePicker.vue.d.ts +4 -0
  30. package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js +6 -2
  31. package/dist/components/date-time-picker/RuiDateTimePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  32. package/dist/components/date-time-picker/RuiDateTimePickerMenu.js.map +1 -1
  33. package/dist/components/date-time-picker/RuiDateTimePickerMenu.vue_vue_type_script_setup_true_lang.js +1 -1
  34. package/dist/components/date-time-picker/RuiDateTimePickerMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  35. package/dist/components/date-time-picker/RuiTimezoneSelect.js.map +1 -1
  36. package/dist/components/date-time-picker/RuiTimezoneSelect.vue.d.ts +4 -0
  37. package/dist/components/date-time-picker/RuiTimezoneSelect.vue_vue_type_script_setup_true_lang.js +3 -2
  38. package/dist/components/date-time-picker/RuiTimezoneSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
  39. package/dist/components/forms/auto-complete/RuiAutoComplete.js.map +1 -1
  40. package/dist/components/forms/auto-complete/RuiAutoComplete.vue_vue_type_script_setup_true_lang.js +6 -2
  41. package/dist/components/forms/auto-complete/RuiAutoComplete.vue_vue_type_script_setup_true_lang.js.map +1 -1
  42. package/dist/components/forms/auto-complete/RuiAutoCompleteSelection.js.map +1 -1
  43. package/dist/components/forms/auto-complete/RuiAutoCompleteSelection.vue_vue_type_script_setup_true_lang.js +9 -6
  44. package/dist/components/forms/auto-complete/RuiAutoCompleteSelection.vue_vue_type_script_setup_true_lang.js.map +1 -1
  45. package/dist/components/forms/auto-complete/auto-complete-props.d.ts +4 -0
  46. package/dist/components/forms/category-picker/RuiCategoryPicker.js.map +1 -1
  47. package/dist/components/forms/category-picker/RuiCategoryPicker.vue.d.ts +4 -1
  48. package/dist/components/forms/category-picker/RuiCategoryPicker.vue_vue_type_script_setup_true_lang.js +7 -4
  49. package/dist/components/forms/category-picker/RuiCategoryPicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
  50. package/dist/components/forms/check-control-styles.d.ts +27 -0
  51. package/dist/components/forms/check-control-styles.js +26 -1
  52. package/dist/components/forms/check-control-styles.js.map +1 -1
  53. package/dist/components/forms/checkbox/RuiCheckbox.js.map +1 -1
  54. package/dist/components/forms/checkbox/RuiCheckbox.vue.d.ts +6 -0
  55. package/dist/components/forms/checkbox/RuiCheckbox.vue_vue_type_script_setup_true_lang.js +14 -4
  56. package/dist/components/forms/checkbox/RuiCheckbox.vue_vue_type_script_setup_true_lang.js.map +1 -1
  57. package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.js.map +1 -1
  58. package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.vue.d.ts +4 -0
  59. package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.vue_vue_type_script_setup_true_lang.js +7 -2
  60. package/dist/components/forms/checkbox/checkbox-group/RuiCheckboxGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  61. package/dist/components/forms/field-defaults/RuiFieldDefaults.js.map +1 -1
  62. package/dist/components/forms/field-defaults/RuiFieldDefaults.vue.d.ts +3 -0
  63. package/dist/components/forms/field-defaults/RuiFieldDefaults.vue_vue_type_script_setup_true_lang.js +8 -2
  64. package/dist/components/forms/field-defaults/RuiFieldDefaults.vue_vue_type_script_setup_true_lang.js.map +1 -1
  65. package/dist/components/forms/file-upload/RuiFileUpload.js.map +1 -1
  66. package/dist/components/forms/file-upload/RuiFileUpload.vue.d.ts +4 -1
  67. package/dist/components/forms/file-upload/RuiFileUpload.vue_vue_type_script_setup_true_lang.js +10 -3
  68. package/dist/components/forms/file-upload/RuiFileUpload.vue_vue_type_script_setup_true_lang.js.map +1 -1
  69. package/dist/components/forms/radio-button/radio/RuiRadio.js.map +1 -1
  70. package/dist/components/forms/radio-button/radio/RuiRadio.vue.d.ts +6 -0
  71. package/dist/components/forms/radio-button/radio/RuiRadio.vue_vue_type_script_setup_true_lang.js +14 -3
  72. package/dist/components/forms/radio-button/radio/RuiRadio.vue_vue_type_script_setup_true_lang.js.map +1 -1
  73. package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.js.map +1 -1
  74. package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.vue.d.ts +4 -0
  75. package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.vue_vue_type_script_setup_true_lang.js +7 -2
  76. package/dist/components/forms/radio-button/radio-group/RuiRadioGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  77. package/dist/components/forms/revealable-text-field/RuiRevealableTextField.js.map +1 -1
  78. package/dist/components/forms/revealable-text-field/RuiRevealableTextField.vue_vue_type_script_setup_true_lang.js +3 -2
  79. package/dist/components/forms/revealable-text-field/RuiRevealableTextField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  80. package/dist/components/forms/select/RuiMenuSelect.js.map +1 -1
  81. package/dist/components/forms/select/RuiMenuSelect.vue.d.ts +4 -0
  82. package/dist/components/forms/select/RuiMenuSelect.vue_vue_type_script_setup_true_lang.js +6 -2
  83. package/dist/components/forms/select/RuiMenuSelect.vue_vue_type_script_setup_true_lang.js.map +1 -1
  84. package/dist/components/forms/slider/RuiSlider.js.map +1 -1
  85. package/dist/components/forms/slider/RuiSlider.vue.d.ts +4 -0
  86. package/dist/components/forms/slider/RuiSlider.vue_vue_type_script_setup_true_lang.js +6 -3
  87. package/dist/components/forms/slider/RuiSlider.vue_vue_type_script_setup_true_lang.js.map +1 -1
  88. package/dist/components/forms/switch/RuiSwitch.js.map +1 -1
  89. package/dist/components/forms/switch/RuiSwitch.vue.d.ts +6 -0
  90. package/dist/components/forms/switch/RuiSwitch.vue_vue_type_script_setup_true_lang.js +25 -5
  91. package/dist/components/forms/switch/RuiSwitch.vue_vue_type_script_setup_true_lang.js.map +1 -1
  92. package/dist/components/forms/text-area/RuiTextArea.js.map +1 -1
  93. package/dist/components/forms/text-area/RuiTextArea.vue.d.ts +4 -0
  94. package/dist/components/forms/text-area/RuiTextArea.vue_vue_type_script_setup_true_lang.js +6 -4
  95. package/dist/components/forms/text-area/RuiTextArea.vue_vue_type_script_setup_true_lang.js.map +1 -1
  96. package/dist/components/forms/text-area/text-area-styles.d.ts +0 -1
  97. package/dist/components/forms/text-area/text-area-styles.js +0 -1
  98. package/dist/components/forms/text-area/text-area-styles.js.map +1 -1
  99. package/dist/components/forms/text-field/RuiTextField.js.map +1 -1
  100. package/dist/components/forms/text-field/RuiTextField.vue.d.ts +6 -2
  101. package/dist/components/forms/text-field/RuiTextField.vue_vue_type_script_setup_true_lang.js +6 -4
  102. package/dist/components/forms/text-field/RuiTextField.vue_vue_type_script_setup_true_lang.js.map +1 -1
  103. package/dist/components/forms/text-field/text-field-styles.d.ts +0 -1
  104. package/dist/components/forms/text-field/text-field-styles.js +0 -1
  105. package/dist/components/forms/text-field/text-field-styles.js.map +1 -1
  106. package/dist/components/helpers/RuiFormTextDetail.js.map +1 -1
  107. package/dist/components/helpers/RuiFormTextDetail.vue.d.ts +14 -0
  108. package/dist/components/helpers/RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js +41 -11
  109. package/dist/components/helpers/RuiFormTextDetail.vue_vue_type_script_setup_true_lang.js.map +1 -1
  110. package/dist/components/loaders/RuiSkeletonBase.js.map +1 -1
  111. package/dist/components/loaders/RuiSkeletonBase.vue_vue_type_script_setup_true_lang.js.map +1 -1
  112. package/dist/components/overlays/menu/RuiMenu.js.map +1 -1
  113. package/dist/components/overlays/menu/RuiMenu.vue.d.ts +3 -0
  114. package/dist/components/overlays/menu/RuiMenu.vue_vue_type_script_setup_true_lang.js +9 -1
  115. package/dist/components/overlays/menu/RuiMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  116. package/dist/components/tables/RuiDataTable.js.map +1 -1
  117. package/dist/components/tables/RuiDataTable.vue.d.ts +9 -0
  118. package/dist/components/tables/RuiDataTable.vue_vue_type_script_setup_true_lang.js +19 -7
  119. package/dist/components/tables/RuiDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  120. package/dist/components/tables/RuiExpandButton.js.map +1 -1
  121. package/dist/components/tables/RuiExpandButton.vue_vue_type_script_setup_true_lang.js.map +1 -1
  122. package/dist/components/tables/RuiTableHead.js.map +1 -1
  123. package/dist/components/tables/RuiTableHead.vue_vue_type_script_setup_true_lang.js +1 -1
  124. package/dist/components/tables/RuiTableHead.vue_vue_type_script_setup_true_lang.js.map +1 -1
  125. package/dist/components/tables/RuiTablePagination.js.map +1 -1
  126. package/dist/components/tables/RuiTablePagination.vue_vue_type_script_setup_true_lang.js +3 -3
  127. package/dist/components/tables/RuiTablePagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  128. package/dist/components/tables/data-table/RuiDataTableAppendRow.js.map +1 -1
  129. package/dist/components/tables/data-table/RuiDataTableAppendRow.vue_vue_type_script_setup_true_lang.js.map +1 -1
  130. package/dist/components/tables/data-table/RuiDataTableBody.js.map +1 -1
  131. package/dist/components/tables/data-table/RuiDataTableBody.vue_vue_type_script_setup_true_lang.js +4 -3
  132. package/dist/components/tables/data-table/RuiDataTableBody.vue_vue_type_script_setup_true_lang.js.map +1 -1
  133. package/dist/components/tables/data-table/RuiDataTableRow.js.map +1 -1
  134. package/dist/components/tables/data-table/RuiDataTableRow.vue_vue_type_script_setup_true_lang.js +4 -4
  135. package/dist/components/tables/data-table/RuiDataTableRow.vue_vue_type_script_setup_true_lang.js.map +1 -1
  136. package/dist/components/tables/data-table-styles.d.ts +6 -0
  137. package/dist/components/tables/data-table-styles.js +15 -7
  138. package/dist/components/tables/data-table-styles.js.map +1 -1
  139. package/dist/components/tabs/tab-items/RuiTabItems.js.map +1 -1
  140. package/dist/components/tabs/tab-items/RuiTabItems.vue_vue_type_script_setup_true_lang.js +9 -2
  141. package/dist/components/tabs/tab-items/RuiTabItems.vue_vue_type_script_setup_true_lang.js.map +1 -1
  142. package/dist/composables/clip-while-resizing.d.ts +15 -0
  143. package/dist/composables/clip-while-resizing.js +46 -0
  144. package/dist/composables/clip-while-resizing.js.map +1 -0
  145. package/dist/composables/defaults/field.d.ts +19 -3
  146. package/dist/composables/defaults/field.js +32 -6
  147. package/dist/composables/defaults/field.js.map +1 -1
  148. package/dist/composables/sticky-blocker.d.ts +20 -0
  149. package/dist/composables/sticky-blocker.js +62 -0
  150. package/dist/composables/sticky-blocker.js.map +1 -0
  151. package/dist/composables/tables/data-table/page-scroll.js +11 -1
  152. package/dist/composables/tables/data-table/page-scroll.js.map +1 -1
  153. package/dist/index.d.ts +6 -2
  154. package/dist/index.js.map +1 -1
  155. package/dist/style.css +352 -11
  156. package/dist/utils/development.d.ts +6 -0
  157. package/dist/utils/development.js +17 -0
  158. package/dist/utils/development.js.map +1 -0
  159. package/dist/utils/form-text-detail.d.ts +15 -0
  160. package/dist/utils/form-text-detail.js +11 -1
  161. package/dist/utils/form-text-detail.js.map +1 -1
  162. package/dist/web-types.json +242 -3
  163. package/package.json +1 -1
@@ -60,6 +60,7 @@ export declare const dataTableStyles: import("tailwind-variants").TVReturnType<{
60
60
  tbody: string;
61
61
  tr: string;
62
62
  td: string;
63
+ tfoot: string;
63
64
  };
64
65
  };
65
66
  }, {
@@ -69,6 +70,7 @@ export declare const dataTableStyles: import("tailwind-variants").TVReturnType<{
69
70
  tbody: string;
70
71
  tr: string;
71
72
  td: string;
73
+ tfoot: string;
72
74
  checkbox: string;
73
75
  tbodyLoader: string;
74
76
  pagination: string;
@@ -128,6 +130,7 @@ export declare const dataTableStyles: import("tailwind-variants").TVReturnType<{
128
130
  tbody: string;
129
131
  tr: string;
130
132
  td: string;
133
+ tfoot: string;
131
134
  };
132
135
  };
133
136
  }, {
@@ -137,6 +140,7 @@ export declare const dataTableStyles: import("tailwind-variants").TVReturnType<{
137
140
  tbody: string;
138
141
  tr: string;
139
142
  td: string;
143
+ tfoot: string;
140
144
  checkbox: string;
141
145
  tbodyLoader: string;
142
146
  pagination: string;
@@ -196,6 +200,7 @@ export declare const dataTableStyles: import("tailwind-variants").TVReturnType<{
196
200
  tbody: string;
197
201
  tr: string;
198
202
  td: string;
203
+ tfoot: string;
199
204
  };
200
205
  };
201
206
  }, {
@@ -205,6 +210,7 @@ export declare const dataTableStyles: import("tailwind-variants").TVReturnType<{
205
210
  tbody: string;
206
211
  tr: string;
207
212
  td: string;
213
+ tfoot: string;
208
214
  checkbox: string;
209
215
  tbodyLoader: string;
210
216
  pagination: string;
@@ -19,8 +19,9 @@ var dataTableStyles = tv({
19
19
  scroller: "overflow-x-auto overflow-y-hidden [clip-path:inset(0_0_0_0)] outline-hidden focus-visible:focus-ring focus-visible:-outline-offset-2",
20
20
  table: "min-w-full table-fixed between:border-t between:border-b-0 between:border-rui-divider whitespace-nowrap mx-auto my-0 max-w-fit relative border-rui-divider",
21
21
  tbody: "between:border-t between:border-b-0 between:border-rui-divider",
22
- tr: "hover:bg-rui-hover",
22
+ tr: "hover:bg-rui-neutral-950/[0.03] dark:hover:bg-white/[0.04]",
23
23
  td: "[:where(&)]:px-4 text-rui-text text-body-2 tabular-nums [text-wrap:initial]",
24
+ tfoot: "[:where(&>tr>*)]:px-4 text-rui-text text-body-2 tabular-nums",
24
25
  checkbox: "px-2 w-[3.625rem] max-w-[3.625rem] [&_label]:ml-0",
25
26
  tbodyLoader: "text-center",
26
27
  pagination: `bg-rui-surface-muted ${NESTED_BAR}`
@@ -37,13 +38,13 @@ var dataTableStyles = tv({
37
38
  true: {},
38
39
  false: {}
39
40
  },
40
- striped: { true: { tbody: "[:where(&>tr[data-stripe])]:bg-rui-neutral-950/[0.03] dark:[:where(&>tr[data-stripe])]:bg-white/[0.04]" } },
41
+ striped: { true: { tbody: "[:where(&>tr[data-stripe])]:bg-rui-neutral-950/[0.015] dark:[:where(&>tr[data-stripe])]:bg-white/[0.02]" } },
41
42
  rowVariant: {
42
43
  selected: { tr: "bg-rui-primary-soft" },
43
- empty: { tr: "hover:bg-transparent" },
44
+ empty: { tr: "hover:bg-transparent dark:hover:bg-transparent" },
44
45
  expandable: { tr: `bg-rui-neutral-950/[0.07] hover:bg-rui-neutral-950/[0.07] dark:bg-white/[0.09] dark:hover:bg-white/[0.09] ${NESTED_BAND} [[data-id=row-expanded]_&]:hover:bg-rui-neutral-950/[0.11] dark:[[data-id=row-expanded]_&]:hover:bg-white/[0.14] ${OPEN_ROW_RAIL} !border-t-0` },
45
46
  expandedParent: { tr: `bg-rui-neutral-950/[0.07] dark:bg-white/[0.09] ${NESTED_BAND} ${OPEN_ROW_RAIL}` },
46
- group: { tr: "bg-rui-neutral-100 dark:bg-white/[0.05]" }
47
+ group: { tr: "bg-rui-neutral-100 hover:bg-rui-neutral-100 dark:bg-white/[0.05] dark:hover:bg-white/[0.05]" }
47
48
  },
48
49
  mobile: { true: {
49
50
  wrapper: "between:border-t-0 border-0 rounded-none overflow-visible",
@@ -51,7 +52,8 @@ var dataTableStyles = tv({
51
52
  table: "block min-w-0 max-w-none w-full whitespace-normal between:border-t-0 border-0",
52
53
  tbody: "block between:border-t-0",
53
54
  tr: "block",
54
- td: "block px-4 py-2"
55
+ td: "block px-4 py-2",
56
+ tfoot: "block [&>tr]:block [:where(&>tr>*)]:block [:where(&>tr>*)]:py-2"
55
57
  } }
56
58
  },
57
59
  /**
@@ -63,11 +65,17 @@ var dataTableStyles = tv({
63
65
  compoundVariants: [{
64
66
  dense: false,
65
67
  mobile: false,
66
- class: { td: "[:where(&)]:py-1 [:where(&)]:h-11" }
68
+ class: {
69
+ td: "[:where(&)]:py-1 [:where(&)]:h-11",
70
+ tfoot: "[:where(&>tr>*)]:py-1 [:where(&>tr>*)]:h-11"
71
+ }
67
72
  }, {
68
73
  dense: true,
69
74
  mobile: false,
70
- class: { td: "[:where(&)]:py-1 [:where(&)]:h-9" }
75
+ class: {
76
+ td: "[:where(&)]:py-1 [:where(&)]:h-9",
77
+ tfoot: "[:where(&>tr>*)]:py-1 [:where(&>tr>*)]:h-9"
78
+ }
71
79
  }],
72
80
  defaultVariants: {
73
81
  dense: false,
@@ -1 +1 @@
1
- {"version":3,"file":"data-table-styles.js","names":[],"sources":["../../../src/components/tables/data-table-styles.ts"],"sourcesContent":["import { tv } from '@/utils/tv';\n\n/** A 2px primary rail down the left edge of an open row and its panel; out of flow, so the cell keeps its padding. */\nconst OPEN_ROW_RAIL = '[&>td:first-child]:relative [&>td:first-child]:before:absolute [&>td:first-child]:before:inset-y-0 [&>td:first-child]:before:left-0 [&>td:first-child]:before:w-0.5 [&>td:first-child]:before:bg-rui-primary';\n\n/**\n * The band of an open row inside another table's panel: a step darker than the outer band, so the two\n * levels never share a grey while the rows between them keep the plain surface.\n */\nconst NESTED_BAND = '[[data-id=row-expanded]_&]:bg-rui-neutral-950/[0.11] dark:[[data-id=row-expanded]_&]:bg-white/[0.14]';\n\n/**\n * Header and pagination of a nested table, stepped by depth to stand off the band around them (about\n * 1.12:1 or more) as well as their own rows: past the lighter band one panel deep, short of the darker\n * band two deep. Nested bars never stick, so the tint can be translucent over the table's own surface.\n */\nexport const NESTED_BAR = '[[data-id=row-expanded]_&]:bg-rui-neutral-950/[0.13] dark:[[data-id=row-expanded]_&]:bg-white/[0.16] [[data-id=row-expanded]_[data-id=row-expanded]_&]:bg-rui-neutral-950/[0.06] dark:[[data-id=row-expanded]_[data-id=row-expanded]_&]:bg-white/[0.07]';\n\nexport const dataTableStyles = tv({\n slots: {\n // `clip`, not `hidden`, which may shrink the item below its rows; its own surface reads the same anywhere\n wrapper: 'relative between:border-t between:border-b-0 between:border-rui-divider overflow-clip bg-rui-surface',\n // focusable only while it scrolls with nothing to tab to (useKeyboardScroll), ring drawn inside\n scroller: 'overflow-x-auto overflow-y-hidden [clip-path:inset(0_0_0_0)] outline-hidden focus-visible:focus-ring focus-visible:-outline-offset-2',\n table: 'min-w-full table-fixed between:border-t between:border-b-0 between:border-rui-divider whitespace-nowrap mx-auto my-0 max-w-fit relative border-rui-divider',\n tbody: 'between:border-t between:border-b-0 between:border-rui-divider',\n tr: 'hover:bg-rui-hover',\n // numbers line up in columns; no slashed zero, which in Inter reads as a second, monospace font beside zero-free values\n td: '[:where(&)]:px-4 text-rui-text text-body-2 tabular-nums [text-wrap:initial]',\n checkbox: 'px-2 w-[3.625rem] max-w-[3.625rem] [&_label]:ml-0',\n tbodyLoader: 'text-center',\n // tinted like the column header, the pair framing the body; opaque, so rows scroll behind a stuck bar\n pagination: `bg-rui-surface-muted ${NESTED_BAR}`,\n },\n variants: {\n sticky: {\n true: { pagination: 'sticky bottom-0 z-rui-raised' },\n },\n outlined: {\n true: { wrapper: 'border border-rui-divider' },\n },\n rounded: {\n sm: { wrapper: 'rounded-rui-sm' },\n md: { wrapper: 'rounded-rui-card' },\n lg: { wrapper: 'rounded-rui-lg' },\n },\n dense: {\n true: {},\n false: {},\n },\n // under every row state through `:where()`; set per item (`data-stripe`), so a follow-up row or open panel shifts none\n striped: {\n true: { tbody: '[:where(&>tr[data-stripe])]:bg-rui-neutral-950/[0.03] dark:[:where(&>tr[data-stripe])]:bg-white/[0.04]' },\n },\n rowVariant: {\n selected: { tr: 'bg-rui-primary-soft' },\n empty: { tr: 'hover:bg-transparent' },\n // a grey band darker than the header so the two never match, a step darker again inside another panel\n expandable: { tr: `bg-rui-neutral-950/[0.07] hover:bg-rui-neutral-950/[0.07] dark:bg-white/[0.09] dark:hover:bg-white/[0.09] ${NESTED_BAND} [[data-id=row-expanded]_&]:hover:bg-rui-neutral-950/[0.11] dark:[[data-id=row-expanded]_&]:hover:bg-white/[0.14] ${OPEN_ROW_RAIL} !border-t-0` },\n expandedParent: { tr: `bg-rui-neutral-950/[0.07] dark:bg-white/[0.09] ${NESTED_BAND} ${OPEN_ROW_RAIL}` },\n group: { tr: 'bg-rui-neutral-100 dark:bg-white/[0.05]' },\n },\n mobile: {\n true: {\n wrapper: 'between:border-t-0 border-0 rounded-none overflow-visible',\n scroller: 'overflow-visible [clip-path:none]',\n table: 'block min-w-0 max-w-none w-full whitespace-normal between:border-t-0 border-0',\n tbody: 'block between:border-t-0',\n tr: 'block',\n td: 'block px-4 py-2',\n },\n },\n },\n /**\n * A fixed row height keeps a row of plain text as tall as one holding an\n * icon button. Padding and floor sit in `:where()` so a consumer's `cellClass`\n * (`py-0` is common) still wins at any stylesheet order, and each density\n * sets its own because the class merger cannot dedupe these.\n */\n compoundVariants: [\n // a 4px inset under the height floor fits a 36px button in a 44px row (a 28px one in a dense 36px row)\n { dense: false, mobile: false, class: { td: '[:where(&)]:py-1 [:where(&)]:h-11' } },\n { dense: true, mobile: false, class: { td: '[:where(&)]:py-1 [:where(&)]:h-9' } },\n ],\n defaultVariants: {\n dense: false,\n mobile: false,\n },\n});\n"],"mappings":";;;AAGA,IAAM,gBAAgB;;;;;AAMtB,IAAM,cAAc;;;;;;AAOpB,IAAa,aAAa;AAE1B,IAAa,kBAAkB,GAAG;CAChC,OAAO;EAEL,SAAS;EAET,UAAU;EACV,OAAO;EACP,OAAO;EACP,IAAI;EAEJ,IAAI;EACJ,UAAU;EACV,aAAa;EAEb,YAAY,wBAAwB;CACtC;CACA,UAAU;EACR,QAAQ,EACN,MAAM,EAAE,YAAY,+BAA+B,EACrD;EACA,UAAU,EACR,MAAM,EAAE,SAAS,4BAA4B,EAC/C;EACA,SAAS;GACP,IAAI,EAAE,SAAS,iBAAiB;GAChC,IAAI,EAAE,SAAS,mBAAmB;GAClC,IAAI,EAAE,SAAS,iBAAiB;EAClC;EACA,OAAO;GACL,MAAM,CAAC;GACP,OAAO,CAAC;EACV;EAEA,SAAS,EACP,MAAM,EAAE,OAAO,yGAAyG,EAC1H;EACA,YAAY;GACV,UAAU,EAAE,IAAI,sBAAsB;GACtC,OAAO,EAAE,IAAI,uBAAuB;GAEpC,YAAY,EAAE,IAAI,6GAA6G,YAAY,oHAAoH,cAAc,cAAc;GAC3R,gBAAgB,EAAE,IAAI,kDAAkD,YAAY,GAAG,gBAAgB;GACvG,OAAO,EAAE,IAAI,0CAA0C;EACzD;EACA,QAAQ,EACN,MAAM;GACJ,SAAS;GACT,UAAU;GACV,OAAO;GACP,OAAO;GACP,IAAI;GACJ,IAAI;EACN,EACF;CACF;;;;;;;CAOA,kBAAkB,CAEhB;EAAE,OAAO;EAAO,QAAQ;EAAO,OAAO,EAAE,IAAI,oCAAoC;CAAE,GAClF;EAAE,OAAO;EAAM,QAAQ;EAAO,OAAO,EAAE,IAAI,mCAAmC;CAAE,CAClF;CACA,iBAAiB;EACf,OAAO;EACP,QAAQ;CACV;AACF,CAAC"}
1
+ {"version":3,"file":"data-table-styles.js","names":[],"sources":["../../../src/components/tables/data-table-styles.ts"],"sourcesContent":["import { tv } from '@/utils/tv';\n\n/** A 2px primary rail down the left edge of an open row and its panel; out of flow, so the cell keeps its padding. */\nconst OPEN_ROW_RAIL = '[&>td:first-child]:relative [&>td:first-child]:before:absolute [&>td:first-child]:before:inset-y-0 [&>td:first-child]:before:left-0 [&>td:first-child]:before:w-0.5 [&>td:first-child]:before:bg-rui-primary';\n\n/**\n * The band of an open row inside another table's panel: a step darker than the outer band, so the two\n * levels never share a grey while the rows between them keep the plain surface.\n */\nconst NESTED_BAND = '[[data-id=row-expanded]_&]:bg-rui-neutral-950/[0.11] dark:[[data-id=row-expanded]_&]:bg-white/[0.14]';\n\n/**\n * Header and pagination of a nested table, stepped by depth to stand off the band around them (about\n * 1.12:1 or more) as well as their own rows: past the lighter band one panel deep, short of the darker\n * band two deep. Nested bars never stick, so the tint can be translucent over the table's own surface.\n */\nexport const NESTED_BAR = '[[data-id=row-expanded]_&]:bg-rui-neutral-950/[0.13] dark:[[data-id=row-expanded]_&]:bg-white/[0.16] [[data-id=row-expanded]_[data-id=row-expanded]_&]:bg-rui-neutral-950/[0.06] dark:[[data-id=row-expanded]_[data-id=row-expanded]_&]:bg-white/[0.07]';\n\nexport const dataTableStyles = tv({\n slots: {\n // `clip`, not `hidden`, which may shrink the item below its rows; its own surface reads the same anywhere\n wrapper: 'relative between:border-t between:border-b-0 between:border-rui-divider overflow-clip bg-rui-surface',\n // focusable only while it scrolls with nothing to tab to (useKeyboardScroll), ring drawn inside\n scroller: 'overflow-x-auto overflow-y-hidden [clip-path:inset(0_0_0_0)] outline-hidden focus-visible:focus-ring focus-visible:-outline-offset-2',\n table: 'min-w-full table-fixed between:border-t between:border-b-0 between:border-rui-divider whitespace-nowrap mx-auto my-0 max-w-fit relative border-rui-divider',\n tbody: 'between:border-t between:border-b-0 between:border-rui-divider',\n // its own tint, lighter than `rui-hover`, which lands on the header and pagination grey\n tr: 'hover:bg-rui-neutral-950/[0.03] dark:hover:bg-white/[0.04]',\n // numbers line up in columns; no slashed zero, which in Inter reads as a second, monospace font beside zero-free values\n td: '[:where(&)]:px-4 text-rui-text text-body-2 tabular-nums [text-wrap:initial]',\n // the `tfoot` slot's cells (a total row) take the body cells' padding, height and text, so a footer row lines up without its own classes\n tfoot: '[:where(&>tr>*)]:px-4 text-rui-text text-body-2 tabular-nums',\n checkbox: 'px-2 w-[3.625rem] max-w-[3.625rem] [&_label]:ml-0',\n tbodyLoader: 'text-center',\n // tinted like the column header, the pair framing the body; opaque, so rows scroll behind a stuck bar\n pagination: `bg-rui-surface-muted ${NESTED_BAR}`,\n },\n variants: {\n sticky: {\n true: { pagination: 'sticky bottom-0 z-rui-raised' },\n },\n outlined: {\n true: { wrapper: 'border border-rui-divider' },\n },\n rounded: {\n sm: { wrapper: 'rounded-rui-sm' },\n md: { wrapper: 'rounded-rui-card' },\n lg: { wrapper: 'rounded-rui-lg' },\n },\n dense: {\n true: {},\n false: {},\n },\n // under every row state through `:where()`, per item (`data-stripe`); half the row hover, so a striped row still darkens on hover\n striped: {\n true: { tbody: '[:where(&>tr[data-stripe])]:bg-rui-neutral-950/[0.015] dark:[:where(&>tr[data-stripe])]:bg-white/[0.02]' },\n },\n rowVariant: {\n selected: { tr: 'bg-rui-primary-soft' },\n empty: { tr: 'hover:bg-transparent dark:hover:bg-transparent' },\n // a grey band darker than the header so the two never match, a step darker again inside another panel\n expandable: { tr: `bg-rui-neutral-950/[0.07] hover:bg-rui-neutral-950/[0.07] dark:bg-white/[0.09] dark:hover:bg-white/[0.09] ${NESTED_BAND} [[data-id=row-expanded]_&]:hover:bg-rui-neutral-950/[0.11] dark:[[data-id=row-expanded]_&]:hover:bg-white/[0.14] ${OPEN_ROW_RAIL} !border-t-0` },\n expandedParent: { tr: `bg-rui-neutral-950/[0.07] dark:bg-white/[0.09] ${NESTED_BAND} ${OPEN_ROW_RAIL}` },\n group: { tr: 'bg-rui-neutral-100 hover:bg-rui-neutral-100 dark:bg-white/[0.05] dark:hover:bg-white/[0.05]' },\n },\n mobile: {\n true: {\n wrapper: 'between:border-t-0 border-0 rounded-none overflow-visible',\n scroller: 'overflow-visible [clip-path:none]',\n table: 'block min-w-0 max-w-none w-full whitespace-normal between:border-t-0 border-0',\n tbody: 'block between:border-t-0',\n tr: 'block',\n td: 'block px-4 py-2',\n tfoot: 'block [&>tr]:block [:where(&>tr>*)]:block [:where(&>tr>*)]:py-2',\n },\n },\n },\n /**\n * A fixed row height keeps a row of plain text as tall as one holding an\n * icon button. Padding and floor sit in `:where()` so a consumer's `cellClass`\n * (`py-0` is common) still wins at any stylesheet order, and each density\n * sets its own because the class merger cannot dedupe these.\n */\n compoundVariants: [\n // a 4px inset under the height floor fits a 36px button in a 44px row (a 28px one in a dense 36px row)\n { dense: false, mobile: false, class: { td: '[:where(&)]:py-1 [:where(&)]:h-11', tfoot: '[:where(&>tr>*)]:py-1 [:where(&>tr>*)]:h-11' } },\n { dense: true, mobile: false, class: { td: '[:where(&)]:py-1 [:where(&)]:h-9', tfoot: '[:where(&>tr>*)]:py-1 [:where(&>tr>*)]:h-9' } },\n ],\n defaultVariants: {\n dense: false,\n mobile: false,\n },\n});\n"],"mappings":";;;AAGA,IAAM,gBAAgB;;;;;AAMtB,IAAM,cAAc;;;;;;AAOpB,IAAa,aAAa;AAE1B,IAAa,kBAAkB,GAAG;CAChC,OAAO;EAEL,SAAS;EAET,UAAU;EACV,OAAO;EACP,OAAO;EAEP,IAAI;EAEJ,IAAI;EAEJ,OAAO;EACP,UAAU;EACV,aAAa;EAEb,YAAY,wBAAwB;CACtC;CACA,UAAU;EACR,QAAQ,EACN,MAAM,EAAE,YAAY,+BAA+B,EACrD;EACA,UAAU,EACR,MAAM,EAAE,SAAS,4BAA4B,EAC/C;EACA,SAAS;GACP,IAAI,EAAE,SAAS,iBAAiB;GAChC,IAAI,EAAE,SAAS,mBAAmB;GAClC,IAAI,EAAE,SAAS,iBAAiB;EAClC;EACA,OAAO;GACL,MAAM,CAAC;GACP,OAAO,CAAC;EACV;EAEA,SAAS,EACP,MAAM,EAAE,OAAO,0GAA0G,EAC3H;EACA,YAAY;GACV,UAAU,EAAE,IAAI,sBAAsB;GACtC,OAAO,EAAE,IAAI,iDAAiD;GAE9D,YAAY,EAAE,IAAI,6GAA6G,YAAY,oHAAoH,cAAc,cAAc;GAC3R,gBAAgB,EAAE,IAAI,kDAAkD,YAAY,GAAG,gBAAgB;GACvG,OAAO,EAAE,IAAI,8FAA8F;EAC7G;EACA,QAAQ,EACN,MAAM;GACJ,SAAS;GACT,UAAU;GACV,OAAO;GACP,OAAO;GACP,IAAI;GACJ,IAAI;GACJ,OAAO;EACT,EACF;CACF;;;;;;;CAOA,kBAAkB,CAEhB;EAAE,OAAO;EAAO,QAAQ;EAAO,OAAO;GAAE,IAAI;GAAqC,OAAO;EAA8C;CAAE,GACxI;EAAE,OAAO;EAAM,QAAQ;EAAO,OAAO;GAAE,IAAI;GAAoC,OAAO;EAA6C;CAAE,CACvI;CACA,iBAAiB;EACf,OAAO;EACP,QAAQ;CACV;AACF,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"RuiTabItems.js","names":["_sfc_main"],"sources":["../../../../src/components/tabs/tab-items/RuiTabItems.vue"],"sourcesContent":["<script lang=\"ts\" setup generic=\"T extends string | number\">\nimport { Fragment, isVNode } from 'vue';\n\ndefineOptions({\n name: 'RuiTabItems',\n inheritAttrs: false,\n});\n\n/**\n * The tab whose panel is showing.\n *\n * The default matches RuiTabs. Without one, binding both components to the\n * same ref that starts as `undefined` leaves this component with\n * `modelValue === undefined`, so the `active = modelValue === value` check\n * fails for every tab: the panel renders but stays at height 0, which reads as\n * an element with no content.\n */\nconst modelValue = defineModel<T>({ default: () => 0 as T });\n\nconst slots = useSlots();\n\nconst currIndex = ref<number>(-1);\nconst activeIndex = ref<number>(-1);\nconst inner = useTemplateRef<HTMLDivElement>('inner');\n\nconst { height: innerHeight } = useElementSize(inner);\n\nconst children = computed<VNode[]>(() => {\n const slotContent = slots.default?.() ?? [];\n\n const tabs = getChildrenTabs(slotContent);\n\n let anyActive = false;\n const children = tabs.map((tab, index) => {\n const value = tab.props?.value ?? index;\n const active = get(modelValue) === value;\n if (active) {\n anyActive = true;\n if (index !== get(currIndex))\n set(currIndex, index);\n }\n\n return {\n ...tab,\n props: {\n value,\n ...tab.props,\n },\n };\n });\n\n if (!anyActive)\n set(currIndex, -1);\n\n return children;\n});\n\n/**\n * Unwraps the fragments a `v-for` in the slot produces, so only tab items come\n * back.\n *\n * @param children - the slot's vnodes\n * @returns the tab items among them\n */\nfunction getChildrenTabs(children: VNode[]): VNode[] {\n return children\n .flatMap((item) => {\n if (item.type === Fragment && Array.isArray(item.children) && item.children.length > 0)\n return getChildrenTabs(item.children.filter(isVNode));\n\n return [item];\n })\n .flat();\n}\n\nwatch(currIndex, (index) => {\n nextTick(() => {\n set(activeIndex, index);\n });\n});\n</script>\n\n<template>\n <div\n class=\"grow transition-[height] duration-200 ease-out overflow-hidden motion-reduce:transition-none\"\n :style=\"{ height: `${innerHeight}px` }\"\n v-bind=\"$attrs\"\n >\n <div ref=\"inner\">\n <Component\n :is=\"child\"\n v-for=\"(child, i) in children\"\n :key=\"i\"\n :active=\"i === activeIndex\"\n />\n </div>\n </div>\n</template>\n"],"mappings":""}
1
+ {"version":3,"file":"RuiTabItems.js","names":["_sfc_main"],"sources":["../../../../src/components/tabs/tab-items/RuiTabItems.vue"],"sourcesContent":["<script lang=\"ts\" setup generic=\"T extends string | number\">\nimport { Fragment, isVNode } from 'vue';\nimport { useClipWhileResizing } from '@/composables/clip-while-resizing';\n\ndefineOptions({\n name: 'RuiTabItems',\n inheritAttrs: false,\n});\n\n/**\n * The tab whose panel is showing.\n *\n * The default matches RuiTabs. Without one, binding both components to the\n * same ref that starts as `undefined` leaves this component with\n * `modelValue === undefined`, so the `active = modelValue === value` check\n * fails for every tab: the panel renders but stays at height 0, which reads as\n * an element with no content.\n */\nconst modelValue = defineModel<T>({ default: () => 0 as T });\n\nconst slots = useSlots();\n\nconst currIndex = ref<number>(-1);\nconst activeIndex = ref<number>(-1);\nconst inner = useTemplateRef<HTMLDivElement>('inner');\nconst root = useTemplateRef<HTMLDivElement>('root');\n\nconst { height: innerHeight } = useElementSize(inner);\n\n// clip only while the height follows a panel change, so sticky content inside a panel works\nconst clipped = useClipWhileResizing(root, () => get(innerHeight));\n\nconst children = computed<VNode[]>(() => {\n const slotContent = slots.default?.() ?? [];\n\n const tabs = getChildrenTabs(slotContent);\n\n let anyActive = false;\n const children = tabs.map((tab, index) => {\n const value = tab.props?.value ?? index;\n const active = get(modelValue) === value;\n if (active) {\n anyActive = true;\n if (index !== get(currIndex))\n set(currIndex, index);\n }\n\n return {\n ...tab,\n props: {\n value,\n ...tab.props,\n },\n };\n });\n\n if (!anyActive)\n set(currIndex, -1);\n\n return children;\n});\n\n/**\n * Unwraps the fragments a `v-for` in the slot produces, so only tab items come\n * back.\n *\n * @param children - the slot's vnodes\n * @returns the tab items among them\n */\nfunction getChildrenTabs(children: VNode[]): VNode[] {\n return children\n .flatMap((item) => {\n if (item.type === Fragment && Array.isArray(item.children) && item.children.length > 0)\n return getChildrenTabs(item.children.filter(isVNode));\n\n return [item];\n })\n .flat();\n}\n\nwatch(currIndex, (index) => {\n nextTick(() => {\n set(activeIndex, index);\n });\n});\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"grow transition-[height] duration-200 ease-out motion-reduce:transition-none\"\n :class=\"{ 'overflow-hidden': clipped }\"\n :data-clipped=\"clipped || undefined\"\n :style=\"{ height: `${innerHeight}px` }\"\n v-bind=\"$attrs\"\n >\n <div ref=\"inner\">\n <Component\n :is=\"child\"\n v-for=\"(child, i) in children\"\n :key=\"i\"\n :active=\"i === activeIndex\"\n />\n </div>\n </div>\n</template>\n"],"mappings":""}
@@ -1,7 +1,9 @@
1
1
  import { get, set } from "../../../vendor/@vueuse/shared/dist/index.js";
2
2
  import { useElementSize } from "../../../vendor/@vueuse/core/dist/index.js";
3
+ import { useClipWhileResizing } from "../../../composables/clip-while-resizing.js";
3
4
  import { Fragment, computed, createBlock, createElementBlock, createElementVNode, defineComponent, isVNode, mergeProps, nextTick, openBlock, ref, renderList, resolveDynamicComponent, unref, useModel, useSlots, useTemplateRef, watch } from "vue";
4
5
  //#region src/components/tabs/tab-items/RuiTabItems.vue?vue&type=script&setup=true&lang.ts
6
+ var _hoisted_1 = ["data-clipped"];
5
7
  var RuiTabItems_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defineComponent({
6
8
  name: "RuiTabItems",
7
9
  inheritAttrs: false,
@@ -26,7 +28,9 @@ var RuiTabItems_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
26
28
  const currIndex = ref(-1);
27
29
  const activeIndex = ref(-1);
28
30
  const inner = useTemplateRef("inner");
31
+ const root = useTemplateRef("root");
29
32
  const { height: innerHeight } = useElementSize(inner);
33
+ const clipped = useClipWhileResizing(root, () => get(innerHeight));
30
34
  const children = computed(() => {
31
35
  const tabs = getChildrenTabs(slots.default?.() ?? []);
32
36
  let anyActive = false;
@@ -67,7 +71,10 @@ var RuiTabItems_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
67
71
  });
68
72
  return (_ctx, _cache) => {
69
73
  return openBlock(), createElementBlock("div", mergeProps({
70
- class: "grow transition-[height] duration-200 ease-out overflow-hidden motion-reduce:transition-none",
74
+ ref_key: "root",
75
+ ref: root,
76
+ class: ["grow transition-[height] duration-200 ease-out motion-reduce:transition-none", { "overflow-hidden": unref(clipped) }],
77
+ "data-clipped": unref(clipped) || void 0,
71
78
  style: { height: `${unref(innerHeight)}px` }
72
79
  }, _ctx.$attrs), [createElementVNode("div", {
73
80
  ref_key: "inner",
@@ -77,7 +84,7 @@ var RuiTabItems_vue_vue_type_script_setup_true_lang_default = /*@__PURE__*/ defi
77
84
  key: i,
78
85
  active: i === unref(activeIndex)
79
86
  }, null, 8, ["active"]);
80
- }), 128))], 512)], 16);
87
+ }), 128))], 512)], 16, _hoisted_1);
81
88
  };
82
89
  }
83
90
  });
@@ -1 +1 @@
1
- {"version":3,"file":"RuiTabItems.vue_vue_type_script_setup_true_lang.js","names":["$attrs","_useModel","_openBlock","_createElementBlock","_mergeProps","_unref","_createElementVNode","_Fragment","_renderList","_createBlock","_resolveDynamicComponent"],"sources":["../../../../src/components/tabs/tab-items/RuiTabItems.vue"],"sourcesContent":["<script lang=\"ts\" setup generic=\"T extends string | number\">\nimport { Fragment, isVNode } from 'vue';\n\ndefineOptions({\n name: 'RuiTabItems',\n inheritAttrs: false,\n});\n\n/**\n * The tab whose panel is showing.\n *\n * The default matches RuiTabs. Without one, binding both components to the\n * same ref that starts as `undefined` leaves this component with\n * `modelValue === undefined`, so the `active = modelValue === value` check\n * fails for every tab: the panel renders but stays at height 0, which reads as\n * an element with no content.\n */\nconst modelValue = defineModel<T>({ default: () => 0 as T });\n\nconst slots = useSlots();\n\nconst currIndex = ref<number>(-1);\nconst activeIndex = ref<number>(-1);\nconst inner = useTemplateRef<HTMLDivElement>('inner');\n\nconst { height: innerHeight } = useElementSize(inner);\n\nconst children = computed<VNode[]>(() => {\n const slotContent = slots.default?.() ?? [];\n\n const tabs = getChildrenTabs(slotContent);\n\n let anyActive = false;\n const children = tabs.map((tab, index) => {\n const value = tab.props?.value ?? index;\n const active = get(modelValue) === value;\n if (active) {\n anyActive = true;\n if (index !== get(currIndex))\n set(currIndex, index);\n }\n\n return {\n ...tab,\n props: {\n value,\n ...tab.props,\n },\n };\n });\n\n if (!anyActive)\n set(currIndex, -1);\n\n return children;\n});\n\n/**\n * Unwraps the fragments a `v-for` in the slot produces, so only tab items come\n * back.\n *\n * @param children - the slot's vnodes\n * @returns the tab items among them\n */\nfunction getChildrenTabs(children: VNode[]): VNode[] {\n return children\n .flatMap((item) => {\n if (item.type === Fragment && Array.isArray(item.children) && item.children.length > 0)\n return getChildrenTabs(item.children.filter(isVNode));\n\n return [item];\n })\n .flat();\n}\n\nwatch(currIndex, (index) => {\n nextTick(() => {\n set(activeIndex, index);\n });\n});\n</script>\n\n<template>\n <div\n class=\"grow transition-[height] duration-200 ease-out overflow-hidden motion-reduce:transition-none\"\n :style=\"{ height: `${innerHeight}px` }\"\n v-bind=\"$attrs\"\n >\n <div ref=\"inner\">\n <Component\n :is=\"child\"\n v-for=\"(child, i) in children\"\n :key=\"i\"\n :active=\"i === activeIndex\"\n />\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;EAiBA,MAAM,aAAaC,SAAc,SAAA,YAA0B;EAE3D,MAAM,QAAQ,SAAS;EAEvB,MAAM,YAAY,IAAY,EAAE;EAChC,MAAM,cAAc,IAAY,EAAE;EAClC,MAAM,QAAQ,eAA+B,OAAO;EAEpD,MAAM,EAAE,QAAQ,gBAAgB,eAAe,KAAK;EAEpD,MAAM,WAAW,eAAwB;GAGvC,MAAM,OAAO,gBAFO,MAAM,UAAU,KAAK,CAAC,CAEF;GAExC,IAAI,YAAY;GAChB,MAAM,WAAW,KAAK,KAAK,KAAK,UAAU;IACxC,MAAM,QAAQ,IAAI,OAAO,SAAS;IAElC,IADe,IAAI,UAAU,MAAM,OACvB;KACV,YAAY;KACZ,IAAI,UAAU,IAAI,SAAS,GACzB,IAAI,WAAW,KAAK;IACxB;IAEA,OAAO;KACL,GAAG;KACH,OAAO;MACL;MACA,GAAG,IAAI;KACT;IACF;GACF,CAAC;GAED,IAAI,CAAC,WACH,IAAI,WAAW,EAAE;GAEnB,OAAO;EACT,CAAC;;;;;;;;EASD,SAAS,gBAAgB,UAA4B;GACnD,OAAO,SACJ,SAAS,SAAS;IACjB,IAAI,KAAK,SAAS,YAAY,MAAM,QAAQ,KAAK,QAAQ,KAAK,KAAK,SAAS,SAAS,GACnF,OAAO,gBAAgB,KAAK,SAAS,OAAO,OAAO,CAAC;IAEtD,OAAO,CAAC,IAAI;GACd,CAAC,CAAA,CACA,KAAK;EACV;EAEA,MAAM,YAAY,UAAU;GAC1B,eAAe;IACb,IAAI,aAAa,KAAK;GACxB,CAAC;EACH,CAAC;;GAIC,OAAAC,UAAA,GAAAC,mBAaM,OAbNC,WAaM;IAZJ,OAAM;IACL,OAAK,EAAA,QAAA,GAAeC,MAAA,WAAA,EAAW,IAAA;GACxBL,GAAAA,KAAAA,MAAM,GAAA,CAEdM,mBAOM,OAAA;IAPG,SAAA;IAAJ,KAAI;GACPJ,GAAAA,EAAAA,UAAA,IAAA,GAAAC,mBAKEI,UAAA,MAAAC,WAHqBH,MAAA,QAAA,IAAb,OAAO,MAAC;IAFlB,OAAAH,UAAA,GAAAO,YAKEC,wBAJK,KAAK,GAAA;KAET,KAAK;KACL,QAAQ,MAAML,MAAA,WAAA"}
1
+ {"version":3,"file":"RuiTabItems.vue_vue_type_script_setup_true_lang.js","names":["$attrs","_useModel","_openBlock","_createElementBlock","_mergeProps","_unref","_createElementVNode","_Fragment","_renderList","_createBlock","_resolveDynamicComponent"],"sources":["../../../../src/components/tabs/tab-items/RuiTabItems.vue"],"sourcesContent":["<script lang=\"ts\" setup generic=\"T extends string | number\">\nimport { Fragment, isVNode } from 'vue';\nimport { useClipWhileResizing } from '@/composables/clip-while-resizing';\n\ndefineOptions({\n name: 'RuiTabItems',\n inheritAttrs: false,\n});\n\n/**\n * The tab whose panel is showing.\n *\n * The default matches RuiTabs. Without one, binding both components to the\n * same ref that starts as `undefined` leaves this component with\n * `modelValue === undefined`, so the `active = modelValue === value` check\n * fails for every tab: the panel renders but stays at height 0, which reads as\n * an element with no content.\n */\nconst modelValue = defineModel<T>({ default: () => 0 as T });\n\nconst slots = useSlots();\n\nconst currIndex = ref<number>(-1);\nconst activeIndex = ref<number>(-1);\nconst inner = useTemplateRef<HTMLDivElement>('inner');\nconst root = useTemplateRef<HTMLDivElement>('root');\n\nconst { height: innerHeight } = useElementSize(inner);\n\n// clip only while the height follows a panel change, so sticky content inside a panel works\nconst clipped = useClipWhileResizing(root, () => get(innerHeight));\n\nconst children = computed<VNode[]>(() => {\n const slotContent = slots.default?.() ?? [];\n\n const tabs = getChildrenTabs(slotContent);\n\n let anyActive = false;\n const children = tabs.map((tab, index) => {\n const value = tab.props?.value ?? index;\n const active = get(modelValue) === value;\n if (active) {\n anyActive = true;\n if (index !== get(currIndex))\n set(currIndex, index);\n }\n\n return {\n ...tab,\n props: {\n value,\n ...tab.props,\n },\n };\n });\n\n if (!anyActive)\n set(currIndex, -1);\n\n return children;\n});\n\n/**\n * Unwraps the fragments a `v-for` in the slot produces, so only tab items come\n * back.\n *\n * @param children - the slot's vnodes\n * @returns the tab items among them\n */\nfunction getChildrenTabs(children: VNode[]): VNode[] {\n return children\n .flatMap((item) => {\n if (item.type === Fragment && Array.isArray(item.children) && item.children.length > 0)\n return getChildrenTabs(item.children.filter(isVNode));\n\n return [item];\n })\n .flat();\n}\n\nwatch(currIndex, (index) => {\n nextTick(() => {\n set(activeIndex, index);\n });\n});\n</script>\n\n<template>\n <div\n ref=\"root\"\n class=\"grow transition-[height] duration-200 ease-out motion-reduce:transition-none\"\n :class=\"{ 'overflow-hidden': clipped }\"\n :data-clipped=\"clipped || undefined\"\n :style=\"{ height: `${innerHeight}px` }\"\n v-bind=\"$attrs\"\n >\n <div ref=\"inner\">\n <Component\n :is=\"child\"\n v-for=\"(child, i) in children\"\n :key=\"i\"\n :active=\"i === activeIndex\"\n />\n </div>\n </div>\n</template>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;EAkBA,MAAM,aAAaC,SAAc,SAAA,YAA0B;EAE3D,MAAM,QAAQ,SAAS;EAEvB,MAAM,YAAY,IAAY,EAAE;EAChC,MAAM,cAAc,IAAY,EAAE;EAClC,MAAM,QAAQ,eAA+B,OAAO;EACpD,MAAM,OAAO,eAA+B,MAAM;EAElD,MAAM,EAAE,QAAQ,gBAAgB,eAAe,KAAK;EAGpD,MAAM,UAAU,qBAAqB,YAAY,IAAI,WAAW,CAAC;EAEjE,MAAM,WAAW,eAAwB;GAGvC,MAAM,OAAO,gBAFO,MAAM,UAAU,KAAK,CAAC,CAEF;GAExC,IAAI,YAAY;GAChB,MAAM,WAAW,KAAK,KAAK,KAAK,UAAU;IACxC,MAAM,QAAQ,IAAI,OAAO,SAAS;IAElC,IADe,IAAI,UAAU,MAAM,OACvB;KACV,YAAY;KACZ,IAAI,UAAU,IAAI,SAAS,GACzB,IAAI,WAAW,KAAK;IACxB;IAEA,OAAO;KACL,GAAG;KACH,OAAO;MACL;MACA,GAAG,IAAI;KACT;IACF;GACF,CAAC;GAED,IAAI,CAAC,WACH,IAAI,WAAW,EAAE;GAEnB,OAAO;EACT,CAAC;;;;;;;;EASD,SAAS,gBAAgB,UAA4B;GACnD,OAAO,SACJ,SAAS,SAAS;IACjB,IAAI,KAAK,SAAS,YAAY,MAAM,QAAQ,KAAK,QAAQ,KAAK,KAAK,SAAS,SAAS,GACnF,OAAO,gBAAgB,KAAK,SAAS,OAAO,OAAO,CAAC;IAEtD,OAAO,CAAC,IAAI;GACd,CAAC,CAAA,CACA,KAAK;EACV;EAEA,MAAM,YAAY,UAAU;GAC1B,eAAe;IACb,IAAI,aAAa,KAAK;GACxB,CAAC;EACH,CAAC;;GAIC,OAAAC,UAAA,GAAAC,mBAgBM,OAhBNC,WAgBM;IAfA,SAAA;IAAJ,KAAI;IACJ,OAAK,CAAC,gFAA8E,EAAA,mBACvDC,MAAA,OAAA,EAAO,CAAA;IACnC,gBAAcA,MAAA,OAAA,KAAW,KAAA;IACzB,OAAK,EAAA,QAAA,GAAeA,MAAA,WAAA,EAAW,IAAA;GACxBL,GAAAA,KAAAA,MAAM,GAAA,CAEdM,mBAOM,OAAA;IAPG,SAAA;IAAJ,KAAI;GACPJ,GAAAA,EAAAA,UAAA,IAAA,GAAAC,mBAKEI,UAAA,MAAAC,WAHqBH,MAAA,QAAA,IAAb,OAAO,MAAC;IAFlB,OAAAH,UAAA,GAAAO,YAKEC,wBAJK,KAAK,GAAA;KAET,KAAK;KACL,QAAQ,MAAML,MAAA,WAAA"}
@@ -0,0 +1,15 @@
1
+ import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';
2
+ /**
3
+ * Whether a box that animates its height should clip its content right now.
4
+ *
5
+ * A permanent `overflow: hidden` makes the box the scroll container of everything inside it, so a
6
+ * `position: sticky` child sticks to the box and never to the page. This clips only while the
7
+ * height changes: from the moment the height value changes until the box's `height` transition
8
+ * ends. The timer covers a change that runs no transition (reduced motion, a hidden box, an equal
9
+ * computed height), which fires no `transitionend`.
10
+ *
11
+ * @param element - the box whose `height` transitions
12
+ * @param height - the height the box is given; any change starts a clip
13
+ * @returns true while the height is changing
14
+ */
15
+ export declare function useClipWhileResizing(element: Readonly<Ref<HTMLElement | null>>, height: MaybeRefOrGetter<string | number>): ComputedRef<boolean>;
@@ -0,0 +1,46 @@
1
+ import { get, set, useTimeoutFn } from "../vendor/@vueuse/shared/dist/index.js";
2
+ import { useEventListener } from "../vendor/@vueuse/core/dist/index.js";
3
+ import { computed, shallowRef, toValue, watch } from "vue";
4
+ //#region src/composables/clip-while-resizing.ts
5
+ /** A little past the 200ms height transition, for when no `transitionend` arrives. */
6
+ var SETTLE_FALLBACK_MS = 300;
7
+ /**
8
+ * Whether a box that animates its height should clip its content right now.
9
+ *
10
+ * A permanent `overflow: hidden` makes the box the scroll container of everything inside it, so a
11
+ * `position: sticky` child sticks to the box and never to the page. This clips only while the
12
+ * height changes: from the moment the height value changes until the box's `height` transition
13
+ * ends. The timer covers a change that runs no transition (reduced motion, a hidden box, an equal
14
+ * computed height), which fires no `transitionend`.
15
+ *
16
+ * @param element - the box whose `height` transitions
17
+ * @param height - the height the box is given; any change starts a clip
18
+ * @returns true while the height is changing
19
+ */
20
+ function useClipWhileResizing(element, height) {
21
+ const resizing = shallowRef(false);
22
+ const { start, stop } = useTimeoutFn(() => {
23
+ set(resizing, false);
24
+ }, SETTLE_FALLBACK_MS, { immediate: false });
25
+ watch(() => toValue(height), () => {
26
+ set(resizing, true);
27
+ start();
28
+ });
29
+ /**
30
+ * Ends the clip when the box's own height transition ends; a child's transition bubbles up too.
31
+ *
32
+ * @param event - the transition event that reached the box
33
+ */
34
+ function settle(event) {
35
+ if (event.target !== get(element) || event.propertyName !== "height") return;
36
+ stop();
37
+ set(resizing, false);
38
+ }
39
+ useEventListener(element, "transitionend", settle);
40
+ useEventListener(element, "transitioncancel", settle);
41
+ return computed(() => get(resizing));
42
+ }
43
+ //#endregion
44
+ export { useClipWhileResizing };
45
+
46
+ //# sourceMappingURL=clip-while-resizing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clip-while-resizing.js","names":[],"sources":["../../src/composables/clip-while-resizing.ts"],"sourcesContent":["import type { ComputedRef, MaybeRefOrGetter, Ref } from 'vue';\nimport { useEventListener, useTimeoutFn } from '@vueuse/core';\n\n/** A little past the 200ms height transition, for when no `transitionend` arrives. */\nconst SETTLE_FALLBACK_MS = 300;\n\n/**\n * Whether a box that animates its height should clip its content right now.\n *\n * A permanent `overflow: hidden` makes the box the scroll container of everything inside it, so a\n * `position: sticky` child sticks to the box and never to the page. This clips only while the\n * height changes: from the moment the height value changes until the box's `height` transition\n * ends. The timer covers a change that runs no transition (reduced motion, a hidden box, an equal\n * computed height), which fires no `transitionend`.\n *\n * @param element - the box whose `height` transitions\n * @param height - the height the box is given; any change starts a clip\n * @returns true while the height is changing\n */\nexport function useClipWhileResizing(\n element: Readonly<Ref<HTMLElement | null>>,\n height: MaybeRefOrGetter<string | number>,\n): ComputedRef<boolean> {\n const resizing = shallowRef<boolean>(false);\n\n const { start, stop } = useTimeoutFn(() => {\n set(resizing, false);\n }, SETTLE_FALLBACK_MS, { immediate: false });\n\n watch(() => toValue(height), () => {\n set(resizing, true);\n start();\n });\n\n /**\n * Ends the clip when the box's own height transition ends; a child's transition bubbles up too.\n *\n * @param event - the transition event that reached the box\n */\n function settle(event: TransitionEvent): void {\n if (event.target !== get(element) || event.propertyName !== 'height')\n return;\n stop();\n set(resizing, false);\n }\n\n useEventListener(element, 'transitionend', settle);\n useEventListener(element, 'transitioncancel', settle);\n\n return computed<boolean>(() => get(resizing));\n}\n"],"mappings":";;;;;AAIA,IAAM,qBAAqB;;;;;;;;;;;;;;AAe3B,SAAgB,qBACd,SACA,QACsB;CACtB,MAAM,WAAW,WAAoB,KAAK;CAE1C,MAAM,EAAE,OAAO,SAAS,mBAAmB;EACzC,IAAI,UAAU,KAAK;CACrB,GAAG,oBAAoB,EAAE,WAAW,MAAM,CAAC;CAE3C,YAAY,QAAQ,MAAM,SAAS;EACjC,IAAI,UAAU,IAAI;EAClB,MAAM;CACR,CAAC;;;;;;CAOD,SAAS,OAAO,OAA8B;EAC5C,IAAI,MAAM,WAAW,IAAI,OAAO,KAAK,MAAM,iBAAiB,UAC1D;EACF,KAAK;EACL,IAAI,UAAU,KAAK;CACrB;CAEA,iBAAiB,SAAS,iBAAiB,MAAM;CACjD,iBAAiB,SAAS,oBAAoB,MAAM;CAEpD,OAAO,eAAwB,IAAI,QAAQ,CAAC;AAC9C"}
@@ -1,4 +1,5 @@
1
- import type { ComputedRef, InjectionKey, MaybeRef, MaybeRefOrGetter } from 'vue';
1
+ import type { FormDetails } from '../../utils/form-text-detail.js';
2
+ import { type ComputedRef, type InjectionKey, type MaybeRef, type MaybeRefOrGetter } from 'vue';
2
3
  /**
3
4
  * Where a field shows its label.
4
5
  *
@@ -9,14 +10,29 @@ import type { ComputedRef, InjectionKey, MaybeRef, MaybeRefOrGetter } from 'vue'
9
10
  export type LabelPlacement = 'top' | 'hidden';
10
11
  export interface FieldOptions {
11
12
  labelPlacement: MaybeRef<LabelPlacement>;
13
+ details: MaybeRef<FormDetails>;
14
+ }
15
+ /** What a `provideFieldDefaults` or `RuiFieldDefaults` scope sets; an unset key falls through to the scope above. */
16
+ export interface FieldScopeOptions {
17
+ labelPlacement?: MaybeRefOrGetter<LabelPlacement | undefined>;
18
+ details?: MaybeRefOrGetter<FormDetails | undefined>;
12
19
  }
13
20
  /** The app-wide default, set through `createRui({ defaults: { field } })`. */
14
21
  export declare const FieldSymbol: InjectionKey<FieldOptions>;
15
22
  export declare function createFieldDefaults(options?: Partial<FieldOptions>): FieldOptions;
16
- /** Sets the label placement for every field rendered below the calling component. */
17
- export declare function provideFieldDefaults(labelPlacement: MaybeRefOrGetter<LabelPlacement | undefined>): void;
23
+ /**
24
+ * Sets defaults for every field rendered below the calling component. A key left unset
25
+ * keeps what an enclosing scope set. Passing a placement alone is the 3.0 form.
26
+ */
27
+ export declare function provideFieldDefaults(options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>): void;
18
28
  /**
19
29
  * The placement a field uses: its own prop, then the nearest scope, then the
20
30
  * app default, then `top`.
21
31
  */
22
32
  export declare function useLabelPlacement(placement: MaybeRefOrGetter<LabelPlacement | undefined>): ComputedRef<LabelPlacement>;
33
+ /**
34
+ * The details mode the scope and app give a control that sets none itself: the nearest
35
+ * scope, then the app default. `undefined` when neither sets one. Outside a component
36
+ * setup there is nothing to inject, so it is always `undefined` there.
37
+ */
38
+ export declare function useInheritedDetails(): () => FormDetails | undefined;
@@ -1,4 +1,4 @@
1
- import { computed, inject, provide, toValue } from "vue";
1
+ import { computed, hasInjectionContext, inject, isRef, provide, toValue } from "vue";
2
2
  //#region src/composables/defaults/field.ts
3
3
  /** The app-wide default, set through `createRui({ defaults: { field } })`. */
4
4
  var FieldSymbol = Symbol.for("rui:field");
@@ -7,12 +7,27 @@ var FieldScopeSymbol = Symbol.for("rui:field-scope");
7
7
  function createFieldDefaults(options) {
8
8
  return {
9
9
  labelPlacement: "top",
10
+ details: "reserve",
10
11
  ...options
11
12
  };
12
13
  }
13
- /** Sets the label placement for every field rendered below the calling component. */
14
- function provideFieldDefaults(labelPlacement) {
15
- provide(FieldScopeSymbol, labelPlacement);
14
+ function isScopeOptions(options) {
15
+ return typeof options === "object" && !isRef(options);
16
+ }
17
+ /**
18
+ * Sets defaults for every field rendered below the calling component. A key left unset
19
+ * keeps what an enclosing scope set. Passing a placement alone is the 3.0 form.
20
+ */
21
+ function provideFieldDefaults(options) {
22
+ const { labelPlacement, details } = isScopeOptions(options) ? options : {
23
+ labelPlacement: options,
24
+ details: void 0
25
+ };
26
+ const parent = inject(FieldScopeSymbol, void 0);
27
+ provide(FieldScopeSymbol, {
28
+ labelPlacement: () => toValue(labelPlacement) ?? parent?.labelPlacement(),
29
+ details: () => toValue(details) ?? parent?.details()
30
+ });
16
31
  }
17
32
  /**
18
33
  * The placement a field uses: its own prop, then the nearest scope, then the
@@ -21,9 +36,20 @@ function provideFieldDefaults(labelPlacement) {
21
36
  function useLabelPlacement(placement) {
22
37
  const scope = inject(FieldScopeSymbol, void 0);
23
38
  const app = inject(FieldSymbol, void 0);
24
- return computed(() => toValue(placement) ?? toValue(scope) ?? (app ? toValue(app.labelPlacement) : void 0) ?? "top");
39
+ return computed(() => toValue(placement) ?? scope?.labelPlacement() ?? (app ? toValue(app.labelPlacement) : void 0) ?? "top");
40
+ }
41
+ /**
42
+ * The details mode the scope and app give a control that sets none itself: the nearest
43
+ * scope, then the app default. `undefined` when neither sets one. Outside a component
44
+ * setup there is nothing to inject, so it is always `undefined` there.
45
+ */
46
+ function useInheritedDetails() {
47
+ if (!hasInjectionContext()) return () => void 0;
48
+ const scope = inject(FieldScopeSymbol, void 0);
49
+ const app = inject(FieldSymbol, void 0);
50
+ return () => scope?.details() ?? (app ? toValue(app.details) : void 0);
25
51
  }
26
52
  //#endregion
27
- export { FieldSymbol, createFieldDefaults, provideFieldDefaults, useLabelPlacement };
53
+ export { FieldSymbol, createFieldDefaults, provideFieldDefaults, useInheritedDetails, useLabelPlacement };
28
54
 
29
55
  //# sourceMappingURL=field.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"field.js","names":[],"sources":["../../../src/composables/defaults/field.ts"],"sourcesContent":["import type { ComputedRef, InjectionKey, MaybeRef, MaybeRefOrGetter } from 'vue';\n\n/**\n * Where a field shows its label.\n *\n * - `top`: a label above a bordered field (the default).\n * - `hidden`: no visible label; the label is still announced to screen readers.\n * For search and filter fields whose placeholder and icon say what they are.\n */\nexport type LabelPlacement = 'top' | 'hidden';\n\nexport interface FieldOptions {\n labelPlacement: MaybeRef<LabelPlacement>;\n}\n\n/** The app-wide default, set through `createRui({ defaults: { field } })`. */\nexport const FieldSymbol: InjectionKey<FieldOptions> = Symbol.for('rui:field');\n\n/** A default for every field below a component, set with `provideFieldDefaults` or `RuiFieldDefaults`. */\nconst FieldScopeSymbol: InjectionKey<MaybeRefOrGetter<LabelPlacement | undefined>> = Symbol.for('rui:field-scope');\n\nexport function createFieldDefaults(options?: Partial<FieldOptions>): FieldOptions {\n return {\n labelPlacement: 'top',\n ...options,\n };\n}\n\n/** Sets the label placement for every field rendered below the calling component. */\nexport function provideFieldDefaults(labelPlacement: MaybeRefOrGetter<LabelPlacement | undefined>): void {\n provide(FieldScopeSymbol, labelPlacement);\n}\n\n/**\n * The placement a field uses: its own prop, then the nearest scope, then the\n * app default, then `top`.\n */\nexport function useLabelPlacement(placement: MaybeRefOrGetter<LabelPlacement | undefined>): ComputedRef<LabelPlacement> {\n const scope = inject(FieldScopeSymbol, undefined);\n const app = inject(FieldSymbol, undefined);\n\n return computed<LabelPlacement>(() =>\n toValue(placement)\n ?? toValue(scope)\n ?? (app ? toValue(app.labelPlacement) : undefined)\n ?? 'top',\n );\n}\n"],"mappings":";;;AAgBA,IAAa,cAA0C,OAAO,IAAI,WAAW;;AAG7E,IAAM,mBAA+E,OAAO,IAAI,iBAAiB;AAEjH,SAAgB,oBAAoB,SAA+C;CACjF,OAAO;EACL,gBAAgB;EAChB,GAAG;CACL;AACF;;AAGA,SAAgB,qBAAqB,gBAAoE;CACvG,QAAQ,kBAAkB,cAAc;AAC1C;;;;;AAMA,SAAgB,kBAAkB,WAAsF;CACtH,MAAM,QAAQ,OAAO,kBAAkB,KAAA,CAAS;CAChD,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,OAAO,eACL,QAAQ,SAAS,KACd,QAAQ,KAAK,MACZ,MAAM,QAAQ,IAAI,cAAc,IAAI,KAAA,MACrC,KACL;AACF"}
1
+ {"version":3,"file":"field.js","names":[],"sources":["../../../src/composables/defaults/field.ts"],"sourcesContent":["import type { FormDetails } from '@/utils/form-text-detail';\nimport { type ComputedRef, hasInjectionContext, type InjectionKey, type MaybeRef, type MaybeRefOrGetter } from 'vue';\n\n/**\n * Where a field shows its label.\n *\n * - `top`: a label above a bordered field (the default).\n * - `hidden`: no visible label; the label is still announced to screen readers.\n * For search and filter fields whose placeholder and icon say what they are.\n */\nexport type LabelPlacement = 'top' | 'hidden';\n\nexport interface FieldOptions {\n labelPlacement: MaybeRef<LabelPlacement>;\n details: MaybeRef<FormDetails>;\n}\n\n/** What a `provideFieldDefaults` or `RuiFieldDefaults` scope sets; an unset key falls through to the scope above. */\nexport interface FieldScopeOptions {\n labelPlacement?: MaybeRefOrGetter<LabelPlacement | undefined>;\n details?: MaybeRefOrGetter<FormDetails | undefined>;\n}\n\ninterface FieldScope {\n labelPlacement: () => LabelPlacement | undefined;\n details: () => FormDetails | undefined;\n}\n\n/** The app-wide default, set through `createRui({ defaults: { field } })`. */\nexport const FieldSymbol: InjectionKey<FieldOptions> = Symbol.for('rui:field');\n\n/** A default for every field below a component, set with `provideFieldDefaults` or `RuiFieldDefaults`. */\nconst FieldScopeSymbol: InjectionKey<FieldScope> = Symbol.for('rui:field-scope');\n\nexport function createFieldDefaults(options?: Partial<FieldOptions>): FieldOptions {\n return {\n labelPlacement: 'top',\n details: 'reserve',\n ...options,\n };\n}\n\nfunction isScopeOptions(\n options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>,\n): options is FieldScopeOptions {\n return typeof options === 'object' && !isRef(options);\n}\n\n/**\n * Sets defaults for every field rendered below the calling component. A key left unset\n * keeps what an enclosing scope set. Passing a placement alone is the 3.0 form.\n */\nexport function provideFieldDefaults(options: FieldScopeOptions | MaybeRefOrGetter<LabelPlacement | undefined>): void {\n const { labelPlacement, details } = isScopeOptions(options) ? options : { labelPlacement: options, details: undefined };\n const parent = inject(FieldScopeSymbol, undefined);\n\n provide(FieldScopeSymbol, {\n labelPlacement: () => toValue(labelPlacement) ?? parent?.labelPlacement(),\n details: () => toValue(details) ?? parent?.details(),\n });\n}\n\n/**\n * The placement a field uses: its own prop, then the nearest scope, then the\n * app default, then `top`.\n */\nexport function useLabelPlacement(placement: MaybeRefOrGetter<LabelPlacement | undefined>): ComputedRef<LabelPlacement> {\n const scope = inject(FieldScopeSymbol, undefined);\n const app = inject(FieldSymbol, undefined);\n\n return computed<LabelPlacement>(() =>\n toValue(placement)\n ?? scope?.labelPlacement()\n ?? (app ? toValue(app.labelPlacement) : undefined)\n ?? 'top',\n );\n}\n\n/**\n * The details mode the scope and app give a control that sets none itself: the nearest\n * scope, then the app default. `undefined` when neither sets one. Outside a component\n * setup there is nothing to inject, so it is always `undefined` there.\n */\nexport function useInheritedDetails(): () => FormDetails | undefined {\n if (!hasInjectionContext())\n return () => undefined;\n\n const scope = inject(FieldScopeSymbol, undefined);\n const app = inject(FieldSymbol, undefined);\n\n return () => scope?.details() ?? (app ? toValue(app.details) : undefined);\n}\n"],"mappings":";;;AA6BA,IAAa,cAA0C,OAAO,IAAI,WAAW;;AAG7E,IAAM,mBAA6C,OAAO,IAAI,iBAAiB;AAE/E,SAAgB,oBAAoB,SAA+C;CACjF,OAAO;EACL,gBAAgB;EAChB,SAAS;EACT,GAAG;CACL;AACF;AAEA,SAAS,eACP,SAC8B;CAC9B,OAAO,OAAO,YAAY,YAAY,CAAC,MAAM,OAAO;AACtD;;;;;AAMA,SAAgB,qBAAqB,SAAiF;CACpH,MAAM,EAAE,gBAAgB,YAAY,eAAe,OAAO,IAAI,UAAU;EAAE,gBAAgB;EAAS,SAAS,KAAA;CAAU;CACtH,MAAM,SAAS,OAAO,kBAAkB,KAAA,CAAS;CAEjD,QAAQ,kBAAkB;EACxB,sBAAsB,QAAQ,cAAc,KAAK,QAAQ,eAAe;EACxE,eAAe,QAAQ,OAAO,KAAK,QAAQ,QAAQ;CACrD,CAAC;AACH;;;;;AAMA,SAAgB,kBAAkB,WAAsF;CACtH,MAAM,QAAQ,OAAO,kBAAkB,KAAA,CAAS;CAChD,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,OAAO,eACL,QAAQ,SAAS,KACd,OAAO,eAAe,MACrB,MAAM,QAAQ,IAAI,cAAc,IAAI,KAAA,MACrC,KACL;AACF;;;;;;AAOA,SAAgB,sBAAqD;CACnE,IAAI,CAAC,oBAAoB,GACvB,aAAa,KAAA;CAEf,MAAM,QAAQ,OAAO,kBAAkB,KAAA,CAAS;CAChD,MAAM,MAAM,OAAO,aAAa,KAAA,CAAS;CAEzC,aAAa,OAAO,QAAQ,MAAM,MAAM,QAAQ,IAAI,OAAO,IAAI,KAAA;AACjE"}
@@ -0,0 +1,20 @@
1
+ import type { MaybeRefOrGetter, Ref } from 'vue';
2
+ /**
3
+ * The nearest ancestor with `overflow: hidden` on either axis, which a `position: sticky` element
4
+ * sticks to instead of the page. `hidden` never scrolls, so the element can never stick. `auto` and
5
+ * `scroll` are left out: those can be real scrolling areas, like a dialog body. `clip` makes no
6
+ * scroll container, so it never blocks. A box marked `data-clipped` clips only while it animates.
7
+ *
8
+ * @param element - where the walk up the tree starts; its own overflow does not count
9
+ * @returns the blocking ancestor, or undefined when nothing blocks the element
10
+ */
11
+ export declare function findStickyBlocker(element: HTMLElement): HTMLElement | undefined;
12
+ /**
13
+ * In a development build, warns once when an ancestor keeps a sticky element from sticking to the
14
+ * page. Silent in production, where the check does not run.
15
+ *
16
+ * @param element - the `position: sticky` box, checked once it is mounted
17
+ * @param enabled - whether the element is meant to stick
18
+ * @param name - what the element is, for the message
19
+ */
20
+ export declare function useStickyBlockerWarning(element: Readonly<Ref<HTMLElement | null | undefined>>, enabled: MaybeRefOrGetter<boolean>, name: string): void;
@@ -0,0 +1,62 @@
1
+ import { get, useTimeoutFn } from "../vendor/@vueuse/shared/dist/index.js";
2
+ import { isDevelopment } from "../utils/development.js";
3
+ import { toValue, watch } from "vue";
4
+ //#region src/composables/sticky-blocker.ts
5
+ /** Long enough for an opening accordion or tab panel to settle before the check reads its overflow. */
6
+ var CHECK_DELAY_MS = 1e3;
7
+ /**
8
+ * The nearest ancestor with `overflow: hidden` on either axis, which a `position: sticky` element
9
+ * sticks to instead of the page. `hidden` never scrolls, so the element can never stick. `auto` and
10
+ * `scroll` are left out: those can be real scrolling areas, like a dialog body. `clip` makes no
11
+ * scroll container, so it never blocks. A box marked `data-clipped` clips only while it animates.
12
+ *
13
+ * @param element - where the walk up the tree starts; its own overflow does not count
14
+ * @returns the blocking ancestor, or undefined when nothing blocks the element
15
+ */
16
+ function findStickyBlocker(element) {
17
+ let parent = element.parentElement;
18
+ while (parent && parent !== document.body && parent !== document.documentElement) {
19
+ const { overflowX, overflowY } = getComputedStyle(parent);
20
+ if ((overflowX === "hidden" || overflowY === "hidden") && !parent.hasAttribute("data-clipped")) return parent;
21
+ parent = parent.parentElement;
22
+ }
23
+ }
24
+ /**
25
+ * A short, recognizable name for an element in a console message.
26
+ *
27
+ * @param element - the element to name
28
+ * @returns its tag, `data-id` and first classes
29
+ */
30
+ function describe(element) {
31
+ const id = element.dataset.id ? `[data-id="${element.dataset.id}"]` : "";
32
+ const classes = [...element.classList].slice(0, 3).map((name) => `.${name}`).join("");
33
+ return `<${element.tagName.toLowerCase()}${id}${classes}>`;
34
+ }
35
+ /**
36
+ * In a development build, warns once when an ancestor keeps a sticky element from sticking to the
37
+ * page. Silent in production, where the check does not run.
38
+ *
39
+ * @param element - the `position: sticky` box, checked once it is mounted
40
+ * @param enabled - whether the element is meant to stick
41
+ * @param name - what the element is, for the message
42
+ */
43
+ function useStickyBlockerWarning(element, enabled, name) {
44
+ if (!isDevelopment()) return;
45
+ let warned = false;
46
+ function check() {
47
+ const target = get(element);
48
+ if (warned || !toValue(enabled) || !target) return;
49
+ const blocker = findStickyBlocker(target);
50
+ if (!blocker) return;
51
+ warned = true;
52
+ console.warn(`[@rotki/ui-library] ${name} cannot stick to the page: the ancestor ${describe(blocker)} has overflow: hidden, so the bar sticks to it instead. Use overflow: clip there (it clips the same without creating a scroll container), or move the element out of it.`, blocker);
53
+ }
54
+ const { start } = useTimeoutFn(check, CHECK_DELAY_MS, { immediate: false });
55
+ watch([element, () => toValue(enabled)], ([target, on]) => {
56
+ if (target && on) start();
57
+ }, { immediate: true });
58
+ }
59
+ //#endregion
60
+ export { findStickyBlocker, useStickyBlockerWarning };
61
+
62
+ //# sourceMappingURL=sticky-blocker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sticky-blocker.js","names":[],"sources":["../../src/composables/sticky-blocker.ts"],"sourcesContent":["import type { MaybeRefOrGetter, Ref } from 'vue';\nimport { useTimeoutFn } from '@vueuse/core';\nimport { isDevelopment } from '@/utils/development';\n\n/** Long enough for an opening accordion or tab panel to settle before the check reads its overflow. */\nconst CHECK_DELAY_MS = 1000;\n\n/**\n * The nearest ancestor with `overflow: hidden` on either axis, which a `position: sticky` element\n * sticks to instead of the page. `hidden` never scrolls, so the element can never stick. `auto` and\n * `scroll` are left out: those can be real scrolling areas, like a dialog body. `clip` makes no\n * scroll container, so it never blocks. A box marked `data-clipped` clips only while it animates.\n *\n * @param element - where the walk up the tree starts; its own overflow does not count\n * @returns the blocking ancestor, or undefined when nothing blocks the element\n */\nexport function findStickyBlocker(element: HTMLElement): HTMLElement | undefined {\n let parent = element.parentElement;\n while (parent && parent !== document.body && parent !== document.documentElement) {\n const { overflowX, overflowY } = getComputedStyle(parent);\n if ((overflowX === 'hidden' || overflowY === 'hidden') && !parent.hasAttribute('data-clipped'))\n return parent;\n parent = parent.parentElement;\n }\n return undefined;\n}\n\n/**\n * A short, recognizable name for an element in a console message.\n *\n * @param element - the element to name\n * @returns its tag, `data-id` and first classes\n */\nfunction describe(element: HTMLElement): string {\n const id = element.dataset.id ? `[data-id=\"${element.dataset.id}\"]` : '';\n const classes = [...element.classList].slice(0, 3).map(name => `.${name}`).join('');\n return `<${element.tagName.toLowerCase()}${id}${classes}>`;\n}\n\n/**\n * In a development build, warns once when an ancestor keeps a sticky element from sticking to the\n * page. Silent in production, where the check does not run.\n *\n * @param element - the `position: sticky` box, checked once it is mounted\n * @param enabled - whether the element is meant to stick\n * @param name - what the element is, for the message\n */\nexport function useStickyBlockerWarning(\n element: Readonly<Ref<HTMLElement | null | undefined>>,\n enabled: MaybeRefOrGetter<boolean>,\n name: string,\n): void {\n if (!isDevelopment())\n return;\n\n let warned = false;\n\n function check(): void {\n const target = get(element);\n if (warned || !toValue(enabled) || !target)\n return;\n const blocker = findStickyBlocker(target);\n if (!blocker)\n return;\n warned = true;\n console.warn(\n `[@rotki/ui-library] ${name} cannot stick to the page: the ancestor ${describe(blocker)} has overflow: hidden, `\n + 'so the bar sticks to it instead. Use overflow: clip there (it clips the same without creating a scroll container), '\n + 'or move the element out of it.',\n blocker,\n );\n }\n\n const { start } = useTimeoutFn(check, CHECK_DELAY_MS, { immediate: false });\n\n watch([element, () => toValue(enabled)], ([target, on]) => {\n if (target && on)\n start();\n }, { immediate: true });\n}\n"],"mappings":";;;;;AAKA,IAAM,iBAAiB;;;;;;;;;;AAWvB,SAAgB,kBAAkB,SAA+C;CAC/E,IAAI,SAAS,QAAQ;CACrB,OAAO,UAAU,WAAW,SAAS,QAAQ,WAAW,SAAS,iBAAiB;EAChF,MAAM,EAAE,WAAW,cAAc,iBAAiB,MAAM;EACxD,KAAK,cAAc,YAAY,cAAc,aAAa,CAAC,OAAO,aAAa,cAAc,GAC3F,OAAO;EACT,SAAS,OAAO;CAClB;AAEF;;;;;;;AAQA,SAAS,SAAS,SAA8B;CAC9C,MAAM,KAAK,QAAQ,QAAQ,KAAK,aAAa,QAAQ,QAAQ,GAAG,MAAM;CACtE,MAAM,UAAU,CAAC,GAAG,QAAQ,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,KAAI,SAAQ,IAAI,MAAM,CAAC,CAAC,KAAK,EAAE;CAClF,OAAO,IAAI,QAAQ,QAAQ,YAAY,IAAI,KAAK,QAAQ;AAC1D;;;;;;;;;AAUA,SAAgB,wBACd,SACA,SACA,MACM;CACN,IAAI,CAAC,cAAc,GACjB;CAEF,IAAI,SAAS;CAEb,SAAS,QAAc;EACrB,MAAM,SAAS,IAAI,OAAO;EAC1B,IAAI,UAAU,CAAC,QAAQ,OAAO,KAAK,CAAC,QAClC;EACF,MAAM,UAAU,kBAAkB,MAAM;EACxC,IAAI,CAAC,SACH;EACF,SAAS;EACT,QAAQ,KACN,uBAAuB,KAAK,0CAA0C,SAAS,OAAO,EAAE,2KAGxF,OACF;CACF;CAEA,MAAM,EAAE,UAAU,aAAa,OAAO,gBAAgB,EAAE,WAAW,MAAM,CAAC;CAE1E,MAAM,CAAC,eAAe,QAAQ,OAAO,CAAC,IAAI,CAAC,QAAQ,QAAQ;EACzD,IAAI,UAAU,IACZ,MAAM;CACV,GAAG,EAAE,WAAW,KAAK,CAAC;AACxB"}
@@ -99,11 +99,21 @@ function useStickyBottomInset(sentinel, bar, canStick) {
99
99
  const { height } = useElementSize(bar, void 0, { box: "border-box" });
100
100
  const { height: viewHeight } = useWindowSize();
101
101
  let frame = 0;
102
- /** From the bar's top to the view's bottom; a stuck bar sits on that bottom, so this is its height. */
102
+ /**
103
+ * From the bar's top to the view's bottom; a stuck bar sits on that bottom, so this is its height.
104
+ * It first checks the sentinel is still below the band: one jump can carry it from below the band to
105
+ * above the view without the observer ever seeing it intersect, which would leave the bar counted
106
+ * while it sits far above the view.
107
+ */
103
108
  function measureCovered() {
104
109
  frame = 0;
105
110
  const element = unrefElement(bar);
106
111
  if (!element) return;
112
+ const place = get(sentinel)?.getBoundingClientRect().top;
113
+ if (place !== void 0 && place < get(viewHeight) - Math.ceil(get(height)) - RESTING_CLEARANCE) {
114
+ set(belowView, false);
115
+ return;
116
+ }
107
117
  set(covered, Math.max(get(height), Math.ceil(get(viewHeight) - element.getBoundingClientRect().top)));
108
118
  }
109
119
  /** A resting bar moves with the page, so while it is in the band, follow it once per frame. */
@@ -1 +1 @@
1
- {"version":3,"file":"page-scroll.js","names":[],"sources":["../../../../src/composables/tables/data-table/page-scroll.ts"],"sourcesContent":["import type { MaybeRefOrGetter, Ref } from 'vue';\nimport type { MaybeElementRef } from '@/types/dom';\n\nexport interface UsePageScrollReturn {\n /** Scroll the table's top into view, if it has scrolled above the visible area. */\n revealTableTop: () => Promise<void>;\n}\n\n/**\n * How much of a table, from its top, must be on screen before its pagination bar sticks: the\n * column header and the first rows. A table just scrolling in from below keeps its bar under the\n * rows, rather than showing the bar on top of the header.\n */\nconst STICK_AFTER = 128;\n\n/**\n * The nearest ancestor that scrolls vertically. `undefined` means the page itself does, through\n * the window or a scrolling `<body>`, which is where a sticky offset such as an app bar applies.\n */\nfunction findScroller(element: HTMLElement): HTMLElement | undefined {\n let node = element.parentElement;\n while (node && node !== document.body && node !== document.documentElement) {\n const { overflowY } = getComputedStyle(node);\n if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight)\n return node;\n node = node.parentElement;\n }\n return undefined;\n}\n\nfunction scrollPageBy(top: number): void {\n const body = document.body;\n if (body.scrollHeight > body.clientHeight && ['auto', 'scroll'].includes(getComputedStyle(body).overflowY))\n body.scrollBy({ top });\n else\n window.scrollBy({ top });\n}\n\n/**\n * After a page change, brings the table's first row back into view so the new page reads from the\n * top. It scrolls only when the table's top sits above the visible area, as it does after paging\n * from the bottom bar of a long page; a table that already shows its top stays put, so paging never\n * makes the view jump.\n */\nexport function usePageScroll(\n wrapper: Readonly<Ref<HTMLElement | null | undefined>>,\n stickyOffset: MaybeRefOrGetter<number | undefined>,\n): UsePageScrollReturn {\n async function revealTableTop(): Promise<void> {\n await nextTick();\n const element = get(wrapper);\n if (!element)\n return;\n\n const scroller = findScroller(element);\n const viewTop = scroller ? scroller.getBoundingClientRect().top : (toValue(stickyOffset) ?? 0);\n const distance = element.getBoundingClientRect().top - viewTop;\n if (distance >= 0)\n return;\n\n if (scroller)\n scroller.scrollBy({ top: distance });\n else\n scrollPageBy(distance);\n }\n\n return { revealTableTop };\n}\n\n/**\n * Whether the pagination bar may stick: true once the top {@link STICK_AFTER}px of the table are\n * inside its scrolling area. It starts true, so a page without IntersectionObserver keeps the bar\n * sticky.\n */\nexport function useStickyPaginationBar(\n wrapper: Readonly<Ref<HTMLElement | null | undefined>>,\n enabled: MaybeRefOrGetter<boolean>,\n): Readonly<Ref<boolean>> {\n const canStick = shallowRef<boolean>(true);\n const root = shallowRef<HTMLElement>();\n\n onMounted(() => {\n const element = get(wrapper);\n if (element)\n set(root, findScroller(element));\n });\n\n useIntersectionObserver(\n () => (toValue(enabled) ? get(wrapper) : undefined),\n ([entry]) => {\n if (!entry)\n return;\n // also true once the table has scrolled past the top, when the bar is out of view anyway\n set(canStick, entry.isIntersecting || entry.boundingClientRect.bottom <= (entry.rootBounds?.top ?? 0));\n },\n { root, rootMargin: `0px 0px -${STICK_AFTER}px 0px` },\n );\n\n return canStick;\n}\n\n/**\n * How far above the bar a bar resting at its own place still counts as covered: a part docked in the\n * bottom corner (rotki's task dock is 82px with its offset) reaches over the bar's top by about this much.\n */\nconst RESTING_CLEARANCE = 48;\n\n/** How much of the view's bottom each pagination bar covers, by table; the page's floating parts clear the most. */\nconst stuckBarHeights = new Map<symbol, number>();\n\nfunction publishStickyBottom(): void {\n if (typeof document === 'undefined')\n return;\n const style = document.documentElement.style;\n if (stuckBarHeights.size === 0)\n style.removeProperty('--rui-sticky-bottom');\n else\n style.setProperty('--rui-sticky-bottom', `${Math.max(...stuckBarHeights.values())}px`);\n}\n\n/**\n * Publishes how much of the view's bottom a stuck pagination bar covers, as `--rui-sticky-bottom` on\n * the document root, so the page's own floating parts (a dock, a FAB) can rise above the bar instead of\n * covering its page buttons. A bar resting at its own place in the bottom band of the view counts too,\n * since it sits in the same corner; it covers from its top down, which is more than its height when it\n * rests above the view's bottom, so the parts keep the same gap above it as above a stuck bar. Only a\n * bar at page level counts: one inside a scrolling area sits under nothing the page floats.\n *\n * @param sentinel - an empty element at the bar's own place after the rows; while it is below the view's bottom band, the bar covers that corner\n * @param bar - the bar, measured for its height\n * @param canStick - whether the bar may stick at all\n */\nexport function useStickyBottomInset(\n sentinel: Readonly<Ref<HTMLElement | null | undefined>>,\n bar: MaybeElementRef,\n canStick: MaybeRefOrGetter<boolean>,\n): void {\n const id = Symbol('sticky-pagination-bar');\n const belowView = shallowRef<boolean>(false);\n const pageLevel = shallowRef<boolean>(false);\n const covered = shallowRef<number>(0);\n const { height } = useElementSize(bar, undefined, { box: 'border-box' });\n const { height: viewHeight } = useWindowSize();\n let frame = 0;\n\n /** From the bar's top to the view's bottom; a stuck bar sits on that bottom, so this is its height. */\n function measureCovered(): void {\n frame = 0;\n const element = unrefElement(bar);\n if (!element)\n return;\n set(covered, Math.max(get(height), Math.ceil(get(viewHeight) - element.getBoundingClientRect().top)));\n }\n\n /** A resting bar moves with the page, so while it is in the band, follow it once per frame. */\n function scheduleMeasure(): void {\n if (get(belowView) && !frame)\n frame = requestAnimationFrame(measureCovered);\n }\n\n useEventListener('scroll', scheduleMeasure, { capture: true, passive: true });\n\n onMounted(() => {\n const element = get(sentinel);\n set(pageLevel, !!element && !findScroller(element));\n });\n\n useIntersectionObserver(sentinel, ([entry]) => {\n if (!entry)\n return;\n // no root bounds (a cross-origin frame) reads as not stuck, which leaves the floating parts where they are\n set(belowView, !entry.isIntersecting && entry.boundingClientRect.top >= (entry.rootBounds?.bottom ?? Number.POSITIVE_INFINITY));\n }, {\n // the view's bottom edge, raised by the band a resting bar covers\n rootMargin: () => `0px 0px -${Math.ceil(get(height)) + RESTING_CLEARANCE}px 0px`,\n });\n\n watch([belowView, height, viewHeight], () => measureCovered());\n\n watchEffect(() => {\n if (get(pageLevel) && toValue(canStick) && get(belowView) && get(height) > 0)\n stuckBarHeights.set(id, Math.max(get(height), get(covered)));\n else\n stuckBarHeights.delete(id);\n publishStickyBottom();\n });\n\n onScopeDispose(() => {\n if (frame)\n cancelAnimationFrame(frame);\n stuckBarHeights.delete(id);\n publishStickyBottom();\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"page-scroll.js","names":[],"sources":["../../../../src/composables/tables/data-table/page-scroll.ts"],"sourcesContent":["import type { MaybeRefOrGetter, Ref } from 'vue';\nimport type { MaybeElementRef } from '@/types/dom';\n\nexport interface UsePageScrollReturn {\n /** Scroll the table's top into view, if it has scrolled above the visible area. */\n revealTableTop: () => Promise<void>;\n}\n\n/**\n * How much of a table, from its top, must be on screen before its pagination bar sticks: the\n * column header and the first rows. A table just scrolling in from below keeps its bar under the\n * rows, rather than showing the bar on top of the header.\n */\nconst STICK_AFTER = 128;\n\n/**\n * The nearest ancestor that scrolls vertically. `undefined` means the page itself does, through\n * the window or a scrolling `<body>`, which is where a sticky offset such as an app bar applies.\n */\nfunction findScroller(element: HTMLElement): HTMLElement | undefined {\n let node = element.parentElement;\n while (node && node !== document.body && node !== document.documentElement) {\n const { overflowY } = getComputedStyle(node);\n if ((overflowY === 'auto' || overflowY === 'scroll') && node.scrollHeight > node.clientHeight)\n return node;\n node = node.parentElement;\n }\n return undefined;\n}\n\nfunction scrollPageBy(top: number): void {\n const body = document.body;\n if (body.scrollHeight > body.clientHeight && ['auto', 'scroll'].includes(getComputedStyle(body).overflowY))\n body.scrollBy({ top });\n else\n window.scrollBy({ top });\n}\n\n/**\n * After a page change, brings the table's first row back into view so the new page reads from the\n * top. It scrolls only when the table's top sits above the visible area, as it does after paging\n * from the bottom bar of a long page; a table that already shows its top stays put, so paging never\n * makes the view jump.\n */\nexport function usePageScroll(\n wrapper: Readonly<Ref<HTMLElement | null | undefined>>,\n stickyOffset: MaybeRefOrGetter<number | undefined>,\n): UsePageScrollReturn {\n async function revealTableTop(): Promise<void> {\n await nextTick();\n const element = get(wrapper);\n if (!element)\n return;\n\n const scroller = findScroller(element);\n const viewTop = scroller ? scroller.getBoundingClientRect().top : (toValue(stickyOffset) ?? 0);\n const distance = element.getBoundingClientRect().top - viewTop;\n if (distance >= 0)\n return;\n\n if (scroller)\n scroller.scrollBy({ top: distance });\n else\n scrollPageBy(distance);\n }\n\n return { revealTableTop };\n}\n\n/**\n * Whether the pagination bar may stick: true once the top {@link STICK_AFTER}px of the table are\n * inside its scrolling area. It starts true, so a page without IntersectionObserver keeps the bar\n * sticky.\n */\nexport function useStickyPaginationBar(\n wrapper: Readonly<Ref<HTMLElement | null | undefined>>,\n enabled: MaybeRefOrGetter<boolean>,\n): Readonly<Ref<boolean>> {\n const canStick = shallowRef<boolean>(true);\n const root = shallowRef<HTMLElement>();\n\n onMounted(() => {\n const element = get(wrapper);\n if (element)\n set(root, findScroller(element));\n });\n\n useIntersectionObserver(\n () => (toValue(enabled) ? get(wrapper) : undefined),\n ([entry]) => {\n if (!entry)\n return;\n // also true once the table has scrolled past the top, when the bar is out of view anyway\n set(canStick, entry.isIntersecting || entry.boundingClientRect.bottom <= (entry.rootBounds?.top ?? 0));\n },\n { root, rootMargin: `0px 0px -${STICK_AFTER}px 0px` },\n );\n\n return canStick;\n}\n\n/**\n * How far above the bar a bar resting at its own place still counts as covered: a part docked in the\n * bottom corner (rotki's task dock is 82px with its offset) reaches over the bar's top by about this much.\n */\nconst RESTING_CLEARANCE = 48;\n\n/** How much of the view's bottom each pagination bar covers, by table; the page's floating parts clear the most. */\nconst stuckBarHeights = new Map<symbol, number>();\n\nfunction publishStickyBottom(): void {\n if (typeof document === 'undefined')\n return;\n const style = document.documentElement.style;\n if (stuckBarHeights.size === 0)\n style.removeProperty('--rui-sticky-bottom');\n else\n style.setProperty('--rui-sticky-bottom', `${Math.max(...stuckBarHeights.values())}px`);\n}\n\n/**\n * Publishes how much of the view's bottom a stuck pagination bar covers, as `--rui-sticky-bottom` on\n * the document root, so the page's own floating parts (a dock, a FAB) can rise above the bar instead of\n * covering its page buttons. A bar resting at its own place in the bottom band of the view counts too,\n * since it sits in the same corner; it covers from its top down, which is more than its height when it\n * rests above the view's bottom, so the parts keep the same gap above it as above a stuck bar. Only a\n * bar at page level counts: one inside a scrolling area sits under nothing the page floats.\n *\n * @param sentinel - an empty element at the bar's own place after the rows; while it is below the view's bottom band, the bar covers that corner\n * @param bar - the bar, measured for its height\n * @param canStick - whether the bar may stick at all\n */\nexport function useStickyBottomInset(\n sentinel: Readonly<Ref<HTMLElement | null | undefined>>,\n bar: MaybeElementRef,\n canStick: MaybeRefOrGetter<boolean>,\n): void {\n const id = Symbol('sticky-pagination-bar');\n const belowView = shallowRef<boolean>(false);\n const pageLevel = shallowRef<boolean>(false);\n const covered = shallowRef<number>(0);\n const { height } = useElementSize(bar, undefined, { box: 'border-box' });\n const { height: viewHeight } = useWindowSize();\n let frame = 0;\n\n /**\n * From the bar's top to the view's bottom; a stuck bar sits on that bottom, so this is its height.\n * It first checks the sentinel is still below the band: one jump can carry it from below the band to\n * above the view without the observer ever seeing it intersect, which would leave the bar counted\n * while it sits far above the view.\n */\n function measureCovered(): void {\n frame = 0;\n const element = unrefElement(bar);\n if (!element)\n return;\n const place = get(sentinel)?.getBoundingClientRect().top;\n if (place !== undefined && place < get(viewHeight) - Math.ceil(get(height)) - RESTING_CLEARANCE) {\n set(belowView, false);\n return;\n }\n set(covered, Math.max(get(height), Math.ceil(get(viewHeight) - element.getBoundingClientRect().top)));\n }\n\n /** A resting bar moves with the page, so while it is in the band, follow it once per frame. */\n function scheduleMeasure(): void {\n if (get(belowView) && !frame)\n frame = requestAnimationFrame(measureCovered);\n }\n\n useEventListener('scroll', scheduleMeasure, { capture: true, passive: true });\n\n onMounted(() => {\n const element = get(sentinel);\n set(pageLevel, !!element && !findScroller(element));\n });\n\n useIntersectionObserver(sentinel, ([entry]) => {\n if (!entry)\n return;\n // no root bounds (a cross-origin frame) reads as not stuck, which leaves the floating parts where they are\n set(belowView, !entry.isIntersecting && entry.boundingClientRect.top >= (entry.rootBounds?.bottom ?? Number.POSITIVE_INFINITY));\n }, {\n // the view's bottom edge, raised by the band a resting bar covers\n rootMargin: () => `0px 0px -${Math.ceil(get(height)) + RESTING_CLEARANCE}px 0px`,\n });\n\n watch([belowView, height, viewHeight], () => measureCovered());\n\n watchEffect(() => {\n if (get(pageLevel) && toValue(canStick) && get(belowView) && get(height) > 0)\n stuckBarHeights.set(id, Math.max(get(height), get(covered)));\n else\n stuckBarHeights.delete(id);\n publishStickyBottom();\n });\n\n onScopeDispose(() => {\n if (frame)\n cancelAnimationFrame(frame);\n stuckBarHeights.delete(id);\n publishStickyBottom();\n });\n}\n"],"mappings":";;;;;;;;;AAaA,IAAM,cAAc;;;;;AAMpB,SAAS,aAAa,SAA+C;CACnE,IAAI,OAAO,QAAQ;CACnB,OAAO,QAAQ,SAAS,SAAS,QAAQ,SAAS,SAAS,iBAAiB;EAC1E,MAAM,EAAE,cAAc,iBAAiB,IAAI;EAC3C,KAAK,cAAc,UAAU,cAAc,aAAa,KAAK,eAAe,KAAK,cAC/E,OAAO;EACT,OAAO,KAAK;CACd;AAEF;AAEA,SAAS,aAAa,KAAmB;CACvC,MAAM,OAAO,SAAS;CACtB,IAAI,KAAK,eAAe,KAAK,gBAAgB,CAAC,QAAQ,QAAQ,CAAC,CAAC,SAAS,iBAAiB,IAAI,CAAC,CAAC,SAAS,GACvG,KAAK,SAAS,EAAE,IAAI,CAAC;MAErB,OAAO,SAAS,EAAE,IAAI,CAAC;AAC3B;;;;;;;AAQA,SAAgB,cACd,SACA,cACqB;CACrB,eAAe,iBAAgC;EAC7C,MAAM,SAAS;EACf,MAAM,UAAU,IAAI,OAAO;EAC3B,IAAI,CAAC,SACH;EAEF,MAAM,WAAW,aAAa,OAAO;EACrC,MAAM,UAAU,WAAW,SAAS,sBAAsB,CAAC,CAAC,MAAO,QAAQ,YAAY,KAAK;EAC5F,MAAM,WAAW,QAAQ,sBAAsB,CAAC,CAAC,MAAM;EACvD,IAAI,YAAY,GACd;EAEF,IAAI,UACF,SAAS,SAAS,EAAE,KAAK,SAAS,CAAC;OAEnC,aAAa,QAAQ;CACzB;CAEA,OAAO,EAAE,eAAe;AAC1B;;;;;;AAOA,SAAgB,uBACd,SACA,SACwB;CACxB,MAAM,WAAW,WAAoB,IAAI;CACzC,MAAM,OAAO,WAAwB;CAErC,gBAAgB;EACd,MAAM,UAAU,IAAI,OAAO;EAC3B,IAAI,SACF,IAAI,MAAM,aAAa,OAAO,CAAC;CACnC,CAAC;CAED,8BACS,QAAQ,OAAO,IAAI,IAAI,OAAO,IAAI,KAAA,IACxC,CAAC,WAAW;EACX,IAAI,CAAC,OACH;EAEF,IAAI,UAAU,MAAM,kBAAkB,MAAM,mBAAmB,WAAW,MAAM,YAAY,OAAO,EAAE;CACvG,GACA;EAAE;EAAM,YAAY,YAAY,YAAY;CAAQ,CACtD;CAEA,OAAO;AACT;;;;;AAMA,IAAM,oBAAoB;;AAG1B,IAAM,kCAAkB,IAAI,IAAoB;AAEhD,SAAS,sBAA4B;CACnC,IAAI,OAAO,aAAa,aACtB;CACF,MAAM,QAAQ,SAAS,gBAAgB;CACvC,IAAI,gBAAgB,SAAS,GAC3B,MAAM,eAAe,qBAAqB;MAE1C,MAAM,YAAY,uBAAuB,GAAG,KAAK,IAAI,GAAG,gBAAgB,OAAO,CAAC,EAAE,GAAG;AACzF;;;;;;;;;;;;;AAcA,SAAgB,qBACd,UACA,KACA,UACM;CACN,MAAM,KAAK,OAAO,uBAAuB;CACzC,MAAM,YAAY,WAAoB,KAAK;CAC3C,MAAM,YAAY,WAAoB,KAAK;CAC3C,MAAM,UAAU,WAAmB,CAAC;CACpC,MAAM,EAAE,WAAW,eAAe,KAAK,KAAA,GAAW,EAAE,KAAK,aAAa,CAAC;CACvE,MAAM,EAAE,QAAQ,eAAe,cAAc;CAC7C,IAAI,QAAQ;;;;;;;CAQZ,SAAS,iBAAuB;EAC9B,QAAQ;EACR,MAAM,UAAU,aAAa,GAAG;EAChC,IAAI,CAAC,SACH;EACF,MAAM,QAAQ,IAAI,QAAQ,CAAC,EAAE,sBAAsB,CAAC,CAAC;EACrD,IAAI,UAAU,KAAA,KAAa,QAAQ,IAAI,UAAU,IAAI,KAAK,KAAK,IAAI,MAAM,CAAC,IAAI,mBAAmB;GAC/F,IAAI,WAAW,KAAK;GACpB;EACF;EACA,IAAI,SAAS,KAAK,IAAI,IAAI,MAAM,GAAG,KAAK,KAAK,IAAI,UAAU,IAAI,QAAQ,sBAAsB,CAAC,CAAC,GAAG,CAAC,CAAC;CACtG;;CAGA,SAAS,kBAAwB;EAC/B,IAAI,IAAI,SAAS,KAAK,CAAC,OACrB,QAAQ,sBAAsB,cAAc;CAChD;CAEA,iBAAiB,UAAU,iBAAiB;EAAE,SAAS;EAAM,SAAS;CAAK,CAAC;CAE5E,gBAAgB;EACd,MAAM,UAAU,IAAI,QAAQ;EAC5B,IAAI,WAAW,CAAC,CAAC,WAAW,CAAC,aAAa,OAAO,CAAC;CACpD,CAAC;CAED,wBAAwB,WAAW,CAAC,WAAW;EAC7C,IAAI,CAAC,OACH;EAEF,IAAI,WAAW,CAAC,MAAM,kBAAkB,MAAM,mBAAmB,QAAQ,MAAM,YAAY,UAAU,OAAO,kBAAkB;CAChI,GAAG,EAED,kBAAkB,YAAY,KAAK,KAAK,IAAI,MAAM,CAAC,IAAI,kBAAkB,QAC3E,CAAC;CAED,MAAM;EAAC;EAAW;EAAQ;CAAU,SAAS,eAAe,CAAC;CAE7D,kBAAkB;EAChB,IAAI,IAAI,SAAS,KAAK,QAAQ,QAAQ,KAAK,IAAI,SAAS,KAAK,IAAI,MAAM,IAAI,GACzE,gBAAgB,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,GAAG,IAAI,OAAO,CAAC,CAAC;OAE3D,gBAAgB,OAAO,EAAE;EAC3B,oBAAoB;CACtB,CAAC;CAED,qBAAqB;EACnB,IAAI,OACF,qBAAqB,KAAK;EAC5B,gBAAgB,OAAO,EAAE;EACzB,oBAAoB;CACtB,CAAC;AACH"}
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ import './style.css';
8
8
  export * from './components/index.js';
9
9
  export { activatorHandlers, type ActivatorHandlers } from './components/overlays/menu/activator-handlers.js';
10
10
  export * from './composables/index.js';
11
- export { type FieldOptions, type LabelPlacement, provideFieldDefaults } from './composables/defaults/field.js';
11
+ export { type FieldOptions, type FieldScopeOptions, type LabelPlacement, provideFieldDefaults } from './composables/defaults/field.js';
12
12
  export type { LogoOptions, LogoResolver } from './composables/defaults/logo.js';
13
13
  export { contextColors, type ContextColorsType } from './consts/colors.js';
14
14
  export { translationKeys } from './i18n/keys.js';
@@ -17,13 +17,17 @@ export { StepperOrientation, StepperState, type StepperStep };
17
17
  export type { VueClassValue } from './types/class-value.js';
18
18
  export { type InitThemeOptions, type ThemeConfig, type ThemeContent, type ThemeData, ThemeMode, } from './types/theme.js';
19
19
  export { createBlockie } from './utils/blockie.js';
20
+ export type { FormDetails } from './utils/form-text-detail.js';
20
21
  export { cn, tv } from './utils/tv.js';
21
22
  export type { VariantProps } from 'tailwind-variants';
22
23
  export interface RuiOptions {
23
24
  theme?: InitThemeOptions;
24
25
  defaults?: {
25
26
  table?: Partial<TableOptions>;
26
- /** `labelPlacement` for every text field, text area, select and picker; `top` when unset. */
27
+ /**
28
+ * `labelPlacement` for every text field, text area, select and picker (`top` when unset),
29
+ * and `details` for every form control (`reserve` when unset).
30
+ */
27
31
  field?: Partial<FieldOptions>;
28
32
  };
29
33
  /**