vectis-ui 0.10.3 → 0.12.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 (57) hide show
  1. package/dist/components/VBadge/VBadge.d.ts +1 -1
  2. package/dist/components/VButton/VButtonGroup.css +1 -1
  3. package/dist/components/VButton/VButtonGroup.d.ts +4 -4
  4. package/dist/components/VButton/VButtonGroup.js.map +1 -1
  5. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js +3 -3
  6. package/dist/components/VButton/VButtonGroup.vue_vue_type_script_setup_true_lang.js.map +1 -1
  7. package/dist/components/VDataTable/VDataTable.css +1 -1
  8. package/dist/components/VDataTable/VDataTable.d.ts +0 -7
  9. package/dist/components/VDataTable/VDataTable.js.map +1 -1
  10. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +121 -126
  11. package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
  12. package/dist/components/VIcon/icons/first_page.d.ts +5 -0
  13. package/dist/components/VIcon/icons/first_page.js +9 -0
  14. package/dist/components/VIcon/icons/first_page.js.map +1 -0
  15. package/dist/components/VIcon/icons/index.d.ts +12 -2
  16. package/dist/components/VIcon/icons/index.js +70 -66
  17. package/dist/components/VIcon/icons/index.js.map +1 -1
  18. package/dist/components/VIcon/icons/last_page.d.ts +5 -0
  19. package/dist/components/VIcon/icons/last_page.js +9 -0
  20. package/dist/components/VIcon/icons/last_page.js.map +1 -0
  21. package/dist/components/VIcon/icons/names.d.ts +2 -2
  22. package/dist/components/VIcon/icons/names.js +1 -1
  23. package/dist/components/VIcon/icons/names.js.map +1 -1
  24. package/dist/components/VMenu/VMenu.d.ts +6 -3
  25. package/dist/components/VMenu/VMenu.js.map +1 -1
  26. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js +34 -31
  27. package/dist/components/VMenu/VMenu.vue_vue_type_script_setup_true_lang.js.map +1 -1
  28. package/dist/components/VMenu/VMenuItem.css +1 -1
  29. package/dist/components/VMenu/VMenuItem.js.map +1 -1
  30. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js +70 -67
  31. package/dist/components/VMenu/VMenuItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  32. package/dist/components/VMenu/VMenuPanel.css +1 -1
  33. package/dist/components/VMenu/VMenuPanel.d.ts +7 -0
  34. package/dist/components/VMenu/VMenuPanel.js.map +1 -1
  35. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js +5 -1
  36. package/dist/components/VMenu/VMenuPanel.vue_vue_type_script_setup_true_lang.js.map +1 -1
  37. package/dist/components/VMenu/context.d.ts +6 -0
  38. package/dist/components/VMenu/context.js +4 -1
  39. package/dist/components/VMenu/context.js.map +1 -1
  40. package/dist/components/VPagination/VPagination.css +1 -1
  41. package/dist/components/VPagination/VPagination.d.ts +32 -16
  42. package/dist/components/VPagination/VPagination.js.map +1 -1
  43. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js +136 -93
  44. package/dist/components/VPagination/VPagination.vue_vue_type_script_setup_true_lang.js.map +1 -1
  45. package/dist/components/VToggle/VToggle.d.ts +5 -5
  46. package/dist/components/VToggle/VToggle.js.map +1 -1
  47. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js +3 -3
  48. package/dist/components/VToggle/VToggle.vue_vue_type_script_setup_true_lang.js.map +1 -1
  49. package/dist/components/VToggle/VToggleItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
  50. package/dist/i18n/en.js +2 -0
  51. package/dist/i18n/en.js.map +1 -1
  52. package/dist/i18n/fr.js +2 -0
  53. package/dist/i18n/fr.js.map +1 -1
  54. package/dist/i18n/types.d.ts +2 -0
  55. package/dist/icons.js +36 -34
  56. package/dist/index.d.ts +1 -1
  57. package/package.json +1 -1
@@ -67,12 +67,12 @@ declare const __VLS_base: import('vue').DefineComponent<BadgeProps, {}, {}, {},
67
67
  variant: BadgeVariant;
68
68
  tone: BadgeTone;
69
69
  icon: IconSource;
70
+ bordered: boolean;
70
71
  color: string;
71
72
  count: number;
72
73
  dot: boolean;
73
74
  overlay: boolean;
74
75
  overlayPosition: BadgeOverlayPosition;
75
- bordered: boolean;
76
76
  ringColor: string;
77
77
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
78
78
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,2 +1,2 @@
1
1
  @layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
2
- @layer vectis.components{.v-button-group{transition:box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);align-items:stretch;display:inline-flex}.v-button-group[data-orientation=vertical]{flex-direction:column}.v-button-group[data-full-width]{inline-size:100%;display:flex}.v-button-group[data-full-width][data-orientation=horizontal]{grid-auto-columns:1fr;grid-auto-flow:column;display:grid}.v-button-group[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))) :is(.v-button,:has(.v-button)):not(:where(.v-overlay,.v-overlay *)),.v-button-group[data-full-width][data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))) :is(.v-button,:has(.v-button)):not(:where(.v-overlay,.v-overlay *)){inline-size:100%}.v-button-group[data-detached]{align-items:center;gap:var(--vectis-space-1)}.v-button-group[data-detached][data-orientation=vertical]{align-items:stretch}.v-button-group:not([data-detached])[data-orientation=horizontal]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(:first-child){margin-inline-start:-1px}.v-button-group:not([data-detached])[data-orientation=horizontal]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-start-start-radius:0;border-end-start-radius:0}.v-button-group:not([data-detached])[data-orientation=horizontal]>.v-button:has(~:not(.v-overlay)),.v-button-group:not([data-detached])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-start-end-radius:0;border-end-end-radius:0}.v-button-group:not([data-detached])[data-orientation=vertical]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(:first-child){margin-block-start:-1px}.v-button-group:not([data-detached])[data-orientation=vertical]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-start-start-radius:0;border-start-end-radius:0}.v-button-group:not([data-detached])[data-orientation=vertical]>.v-button:has(~:not(.v-overlay)),.v-button-group:not([data-detached])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-end-end-radius:0;border-end-start-radius:0}.v-button-group>.v-button,.v-button-group>:not(:where(.v-overlay,.v-button-group)) .v-button:not(:where(.v-overlay *)){position:relative}.v-button-group:not([data-detached]):not([data-seamless])>.v-button:not(:first-child):before,.v-button-group:not([data-detached]):not([data-seamless])>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)):before{content:"";position:absolute}.v-button-group[data-orientation=horizontal]>.v-button:not(:first-child):before,.v-button-group[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)):before{border-inline-start:1px solid var(--vectis-color-border);inset-block:-1px;inset-inline-start:-1px}.v-button-group[data-orientation=vertical]>.v-button:not(:first-child):before,.v-button-group[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)):before{border-block-start:1px solid var(--vectis-color-border);inset-block-start:-1px;inset-inline:-1px}.v-button-group.v-button-group[data-seamless][data-orientation=horizontal]>.v-button:not(:first-child),.v-button-group.v-button-group[data-seamless][data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-inline-start-color:#0000}.v-button-group.v-button-group[data-seamless][data-orientation=horizontal]>.v-button:has(~:not(.v-overlay)),.v-button-group.v-button-group[data-seamless][data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-inline-end-color:#0000}.v-button-group.v-button-group[data-seamless][data-orientation=vertical]>.v-button:not(:first-child),.v-button-group.v-button-group[data-seamless][data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-block-start-color:#0000}.v-button-group.v-button-group[data-seamless][data-orientation=vertical]>.v-button:has(~:not(.v-overlay)),.v-button-group.v-button-group[data-seamless][data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-block-end-color:#0000}.v-button-group:not([data-detached]):has(>.v-button[data-elevated],>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *))){border-radius:var(--vectis-radius-interactive);box-shadow:var(--vectis-shadow-sm)}.v-button-group:not([data-detached]):has(>.v-button[data-elevated]:hover:not(:disabled,[aria-disabled=true]),>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:hover:not(:disabled,[aria-disabled=true]):not(:where(.v-overlay *))){box-shadow:var(--vectis-shadow-md)}.v-button-group:not([data-detached]):has(>.v-button[data-elevated]:active:not(:disabled,[aria-disabled=true]),>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:active:not(:disabled,[aria-disabled=true]):not(:where(.v-overlay *))){box-shadow:var(--vectis-shadow-sm)}.v-button-group.v-button-group:not([data-detached])>.v-button[data-elevated],.v-button-group.v-button-group:not([data-detached])>.v-button[data-elevated]:hover,.v-button-group.v-button-group:not([data-detached])>.v-button[data-elevated]:active,.v-button-group.v-button-group:not([data-detached])>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *)),.v-button-group.v-button-group:not([data-detached])>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:hover:not(:where(.v-overlay *)),.v-button-group.v-button-group:not([data-detached])>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:active:not(:where(.v-overlay *)){box-shadow:none}.v-button-group>.v-button:focus-visible,.v-button-group>:not(:where(.v-overlay,.v-button-group)) .v-button:focus-visible:not(:where(.v-overlay *)){z-index:1}@media (prefers-reduced-motion:reduce){.v-button-group{transition:none}}}
2
+ @layer vectis.components{.v-button-group{transition:box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);align-items:stretch;display:inline-flex}.v-button-group[data-orientation=vertical]{flex-direction:column}.v-button-group[data-full-width]{inline-size:100%;display:flex}.v-button-group[data-full-width][data-orientation=horizontal]{grid-auto-columns:1fr;grid-auto-flow:column;display:grid}.v-button-group[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))) :is(.v-button,:has(.v-button)):not(:where(.v-overlay,.v-overlay *)),.v-button-group[data-full-width][data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))) :is(.v-button,:has(.v-button)):not(:where(.v-overlay,.v-overlay *)){inline-size:100%}.v-button-group[data-detached]{align-items:center;gap:var(--vectis-space-1)}.v-button-group[data-detached][data-orientation=vertical]{align-items:stretch}.v-button-group:not([data-detached])[data-orientation=horizontal]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(:first-child){margin-inline-start:-1px}.v-button-group:not([data-detached])[data-orientation=horizontal]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-start-start-radius:0;border-end-start-radius:0}.v-button-group:not([data-detached])[data-orientation=horizontal]>.v-button:has(~:not(.v-overlay)),.v-button-group:not([data-detached])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-start-end-radius:0;border-end-end-radius:0}.v-button-group:not([data-detached])[data-orientation=vertical]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(:first-child){margin-block-start:-1px}.v-button-group:not([data-detached])[data-orientation=vertical]>.v-button:not(:first-child),.v-button-group:not([data-detached])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-start-start-radius:0;border-start-end-radius:0}.v-button-group:not([data-detached])[data-orientation=vertical]>.v-button:has(~:not(.v-overlay)),.v-button-group:not([data-detached])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-end-end-radius:0;border-end-start-radius:0}.v-button-group>.v-button,.v-button-group>:not(:where(.v-overlay,.v-button-group)) .v-button:not(:where(.v-overlay *)){position:relative}.v-button-group[data-bordered]>.v-button:not(:first-child):before,.v-button-group[data-bordered]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)):before{content:"";position:absolute}.v-button-group[data-orientation=horizontal]>.v-button:not(:first-child):before,.v-button-group[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)):before{border-inline-start:1px solid var(--vectis-color-border);inset-block:-1px;inset-inline-start:-1px}.v-button-group[data-orientation=vertical]>.v-button:not(:first-child):before,.v-button-group[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)):before{border-block-start:1px solid var(--vectis-color-border);inset-block-start:-1px;inset-inline:-1px}.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=horizontal]>.v-button:not(:first-child),.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-inline-start-color:#0000}.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=horizontal]>.v-button:has(~:not(.v-overlay)),.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=horizontal]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-inline-end-color:#0000}.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=vertical]>.v-button:not(:first-child),.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):not(:first-child) .v-button:not(:where(.v-overlay *)){border-block-start-color:#0000}.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=vertical]>.v-button:has(~:not(.v-overlay)),.v-button-group:not([data-detached]):not([data-bordered])[data-orientation=vertical]>:not(:where(.v-overlay,.v-button-group)):has(~:not(.v-overlay)) .v-button:not(:where(.v-overlay *)){border-block-end-color:#0000}.v-button-group:not([data-detached]):has(>.v-button[data-elevated],>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *))){border-radius:var(--vectis-radius-interactive);box-shadow:var(--vectis-shadow-sm)}.v-button-group:not([data-detached]):has(>.v-button[data-elevated]:hover:not(:disabled,[aria-disabled=true]),>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:hover:not(:disabled,[aria-disabled=true]):not(:where(.v-overlay *))){box-shadow:var(--vectis-shadow-md)}.v-button-group:not([data-detached]):has(>.v-button[data-elevated]:active:not(:disabled,[aria-disabled=true]),>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:active:not(:disabled,[aria-disabled=true]):not(:where(.v-overlay *))){box-shadow:var(--vectis-shadow-sm)}.v-button-group.v-button-group:not([data-detached])>.v-button[data-elevated],.v-button-group.v-button-group:not([data-detached])>.v-button[data-elevated]:hover,.v-button-group.v-button-group:not([data-detached])>.v-button[data-elevated]:active,.v-button-group.v-button-group:not([data-detached])>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *)),.v-button-group.v-button-group:not([data-detached])>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:hover:not(:where(.v-overlay *)),.v-button-group.v-button-group:not([data-detached])>:not(:where(.v-overlay,.v-button-group)) .v-button[data-elevated]:active:not(:where(.v-overlay *)){box-shadow:none}.v-button-group>.v-button:focus-visible,.v-button-group>:not(:where(.v-overlay,.v-button-group)) .v-button:focus-visible:not(:where(.v-overlay *)){z-index:1}@media (prefers-reduced-motion:reduce){.v-button-group{transition:none}}}
@@ -13,10 +13,10 @@ interface ButtonGroupProps {
13
13
  */
14
14
  detached?: boolean;
15
15
  /**
16
- * Takes the lines out from between the joined buttons, so the row reads as one frame rather
17
- * than as segments.
16
+ * Draws a line between the joined buttons, so the row reads as segments rather than as one
17
+ * frame. It has no effect under `detached`.
18
18
  */
19
- seamless?: boolean;
19
+ bordered?: boolean;
20
20
  /**
21
21
  * Stretches the row across the whole inline size of its parent, every segment taking an equal
22
22
  * share of that width.
@@ -74,7 +74,7 @@ declare const __VLS_base: import('vue').DefineComponent<ButtonGroupProps, {
74
74
  fullWidth: boolean;
75
75
  disabled: boolean;
76
76
  detached: boolean;
77
- seamless: boolean;
77
+ bordered: boolean;
78
78
  }, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
79
79
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
80
80
  declare const _default: typeof __VLS_export;
@@ -1 +1 @@
1
- {"version":3,"file":"VButtonGroup.js","names":[],"sources":["../../../src/components/VButton/VButtonGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Group shape overrides individual buttons; tone is a fallback and disabled is cumulative. CSS\n * joins borders without changing native activation.\n */\nimport { provide, ref } from 'vue'\n\nimport type { ButtonSize, ButtonTone, ButtonVariant } from './VButton.vue'\nimport { buttonGroupKey } from './context'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\n\n/** Which way the row runs. */\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\n\ninterface ButtonGroupProps {\n /**\n * The direction the buttons are joined in: a row by default, or a column under\n * `vertical`.\n */\n orientation?: ButtonGroupOrientation\n /**\n * Leaves the buttons as separate ones, with a gap between them and each keeping its\n * own corners, instead of joining them into a segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments.\n */\n seamless?: boolean\n /**\n * Stretches the row across the whole inline size of its parent, every segment taking an equal\n * share of that width.\n */\n fullWidth?: boolean\n /**\n * How much visual weight every segment carries, on the values of VButton's own\n * `variant`: `solid`, `outline`, `ghost` or `soft`. It wins over the variant a\n * button inside was given.\n */\n variant?: ButtonVariant\n /** The colour the segments take, among `accent`, `neutral` and `danger`. */\n tone?: ButtonTone\n /**\n * The height of the segments, from the size scale shared by every control:\n * `xs`, `sm`, `md`, `lg` or `xl`. It wins over the size a button inside was given.\n */\n size?: ButtonSize\n /**\n * Takes 4px off the height of every segment. It wins over the value a button inside\n * was given.\n */\n compact?: boolean\n /**\n * Raises every segment off the page, on the terms of VButton's own `elevated`. It\n * wins over the value a button inside was given.\n */\n elevated?: boolean\n /**\n * Makes every segment unusable. A button that disables itself stays disabled either\n * way: the two answers add up rather than one overruling the other.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row, which is a `role=\"group\"`: \"Text formatting\",\n * \"View\". A consumer `aria-label` or `aria-labelledby` wins over it.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ButtonGroupProps>(), {\n // `detached`, `seamless` and `fullWidth` are the group's own layout, and take a real\n // default like the orientation does. The six that follow travel down to the buttons,\n // and are `undefined` by default, the booleans included: it is `undefined`, and not\n // `false`, that means the group has no opinion and lets the button keep its own.\n orientation: 'horizontal',\n detached: false,\n seamless: false,\n fullWidth: false,\n variant: undefined,\n tone: undefined,\n size: undefined,\n compact: undefined,\n elevated: undefined,\n disabled: undefined,\n label: undefined,\n})\n\nconst ariaLabel = useAriaLabel(() => props.label)\n\ndefineSlots<{\n /** The VButtons and VIconButtons to join together. */\n default(): unknown\n}>()\n\n// Getters, so the group's props stay reactive on the other side of the injection.\nprovide(buttonGroupKey, {\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get elevated() {\n return props.elevated\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst groupEl = ref<HTMLElement | null>(null)\n\n// A template ref on this component is not a safe way to its element: the comment above the\n// root makes the template a fragment in development, where `$el` is a text anchor. VToggle,\n// whose root this group is, reads the element from here.\ndefineExpose({\n /** The `role=\"group\"` element. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n `data-seamless` is withheld under `detached`, so the DOM never carries a claim the markup\n cannot honour: apart, the buttons share no edge for a line to sit on.\n -->\n <div\n ref=\"groupEl\"\n class=\"v-button-group\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-orientation=\"orientation\"\n :data-detached=\"detached ? '' : undefined\"\n :data-seamless=\"seamless && !detached ? '' : undefined\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * `.v-overlay` appears in both guards, and neither is optional. A panel must not pass for a\n * segment: VMenu renders its panel as a SIBLING of its trigger, so a group holding a menu has\n * a child that is no segment at all.\n */\n .v-button-group {\n display: inline-flex;\n /* Gives every segment the same height in a row, and the same width in a column,\n whatever each button's own content measures. */\n align-items: stretch;\n /* Declared on every group although only a raised one ever moves its shadow: that is\n what lets the reduced-motion block cancel it with a bare selector rather than\n repeating the elevation `:has()` below. */\n transition: box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-button-group[data-orientation='vertical'] {\n flex-direction: column;\n }\n\n /* An explicit display alongside the inline size, VButton's own full-width argument: an\n inline-level box sits on a line box, and the strut's descender would show as a few\n pixels of dead space under a row asked to fill its parent. */\n .v-button-group[data-full-width] {\n display: flex;\n inline-size: 100%;\n }\n\n /*\n * Grid tracks equalize complete border boxes; flex distributes content boxes and leaves\n * wrapped segments narrower by their missing padding.\n */\n .v-button-group[data-full-width][data-orientation='horizontal'] {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n }\n\n /*\n * The two selectors are what keeps this out of a plain horizontal row, and that is not an\n * optimization. Nothing is stretched there: each segment is as wide as its own content, so a\n * percentage on the button would resolve against a wrapper whose width depends on that very\n * button, which is circular, and a wrapped segment would stop measuring its label.\n */\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)),\n .v-button-group[data-full-width][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)) {\n inline-size: 100%;\n }\n\n /* Detached, the buttons are simply spaced: the base rule already gives the flex box\n and the vertical direction, so a gap and the cross-axis alignment are the whole\n difference. The vertical rule restores the stretching the base declares, which the\n centring above beats by one attribute. */\n .v-button-group[data-detached] {\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-button-group[data-detached][data-orientation='vertical'] {\n align-items: stretch;\n }\n\n /*\n * The negative margin pulls each segment onto its neighbour so their two 1px borders collapse\n * into one line, over which the seam below is then laid: it draws the separation the filled\n * variants would otherwise lack (their border is transparent) and unifies the joint between\n * two outlined ones. Each block is scoped to one orientation on purpose.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-inline-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n }\n\n /*\n * The first segment needs none of that: a panel is rendered after the trigger it belongs to,\n * so it can never take `:first-child` from a segment.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-block-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-start-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-end-start-radius: 0;\n border-end-end-radius: 0;\n }\n\n /*\n * The seam is a box of its own, and not the neighbour's own border, because CSS joins two\n * adjacent borders at a MITRE: where a 1px border meets the transparent top and bottom ones\n * of a solid segment, its last pixel is cut on the diagonal and the bar ends in a notch at\n * each end. A box carrying that border on ONE side has no second border to join, so its ends\n * stay square.\n */\n .v-button-group > .v-button,\n .v-button-group > :not(:where(.v-overlay, .v-button-group)) .v-button:not(:where(.v-overlay *)) {\n position: relative;\n }\n\n /* Only the generating rule is guarded. The two orientation rules below place the insets\n of a pseudo-element that, apart or seamless, is never generated at all. */\n .v-button-group:not([data-detached]):not([data-seamless]) > .v-button:not(:first-child)::before,\n .v-button-group:not([data-detached]):not([data-seamless])\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n content: '';\n position: absolute;\n }\n\n .v-button-group[data-orientation='horizontal'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-block: -1px;\n inset-inline-start: -1px;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n .v-button-group[data-orientation='vertical'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-inline: -1px;\n inset-block-start: -1px;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * The class is doubled to reach (0,6,0), and that is not decoration. Written once these are\n * (0,5,0), which TIES with the compound a component overrides a segment's border through\n * (VToggle's outline frame,\n * `.v-toggle[data-item-variant='outline'] > .v-toggle-item[aria-pressed='true']:is(…)`), and\n * a tie between two sheets is settled by whichever the consumer's bundler put last.\n */\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > .v-button:not(:first-child),\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-inline-start-color: transparent;\n }\n\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-inline-end-color: transparent;\n }\n\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > .v-button:not(:first-child),\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-block-start-color: transparent;\n }\n\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-block-end-color: transparent;\n }\n\n /*\n * Its argument is a list, so a wrapped segment answers for itself, and since a selector list\n * takes the specificity of its heaviest member, the two branches leave the weight where it\n * was. The raised BACKGROUND stays with each button.\n */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated],\n > :not(:where(.v-overlay, .v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *))\n ) {\n border-radius: var(--vectis-radius-interactive);\n box-shadow: var(--vectis-shadow-sm);\n }\n\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-md);\n }\n\n /* After the hover rule and at equal specificity, the VButton order: pressing a\n raised segment settles the row back down. */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-sm);\n }\n\n /*\n * VButton's own hover and active shadows are (0,4,0) on this very element, so at equal\n * specificity the winner would be whichever of the two sheets the consumer's bundler happened\n * to put last.\n */\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated],\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:hover,\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:active,\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:where(.v-overlay *)) {\n box-shadow: none;\n }\n\n /*\n * Raise keyboard focus above the next segment to preserve the shared-edge ring. Do not raise\n * hover/active states, which would cover the neighbour's border seam.\n */\n .v-button-group > .v-button:focus-visible,\n .v-button-group\n > :not(:where(.v-overlay, .v-button-group))\n .v-button:focus-visible:not(:where(.v-overlay *)) {\n z-index: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-button-group {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VButtonGroup.js","names":[],"sources":["../../../src/components/VButton/VButtonGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Group shape overrides individual buttons; tone is a fallback and disabled is cumulative. CSS\n * joins borders without changing native activation.\n */\nimport { provide, ref } from 'vue'\n\nimport type { ButtonSize, ButtonTone, ButtonVariant } from './VButton.vue'\nimport { buttonGroupKey } from './context'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\n\n/** Which way the row runs. */\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\n\ninterface ButtonGroupProps {\n /**\n * The direction the buttons are joined in: a row by default, or a column under\n * `vertical`.\n */\n orientation?: ButtonGroupOrientation\n /**\n * Leaves the buttons as separate ones, with a gap between them and each keeping its\n * own corners, instead of joining them into a segmented control.\n */\n detached?: boolean\n /**\n * Draws a line between the joined buttons, so the row reads as segments rather than as one\n * frame. It has no effect under `detached`.\n */\n bordered?: boolean\n /**\n * Stretches the row across the whole inline size of its parent, every segment taking an equal\n * share of that width.\n */\n fullWidth?: boolean\n /**\n * How much visual weight every segment carries, on the values of VButton's own\n * `variant`: `solid`, `outline`, `ghost` or `soft`. It wins over the variant a\n * button inside was given.\n */\n variant?: ButtonVariant\n /** The colour the segments take, among `accent`, `neutral` and `danger`. */\n tone?: ButtonTone\n /**\n * The height of the segments, from the size scale shared by every control:\n * `xs`, `sm`, `md`, `lg` or `xl`. It wins over the size a button inside was given.\n */\n size?: ButtonSize\n /**\n * Takes 4px off the height of every segment. It wins over the value a button inside\n * was given.\n */\n compact?: boolean\n /**\n * Raises every segment off the page, on the terms of VButton's own `elevated`. It\n * wins over the value a button inside was given.\n */\n elevated?: boolean\n /**\n * Makes every segment unusable. A button that disables itself stays disabled either\n * way: the two answers add up rather than one overruling the other.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row, which is a `role=\"group\"`: \"Text formatting\",\n * \"View\". A consumer `aria-label` or `aria-labelledby` wins over it.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ButtonGroupProps>(), {\n // `detached`, `bordered` and `fullWidth` are the group's own layout, and take a real\n // default like the orientation does. The six that follow travel down to the buttons,\n // and are `undefined` by default, the booleans included: it is `undefined`, and not\n // `false`, that means the group has no opinion and lets the button keep its own.\n orientation: 'horizontal',\n detached: false,\n bordered: false,\n fullWidth: false,\n variant: undefined,\n tone: undefined,\n size: undefined,\n compact: undefined,\n elevated: undefined,\n disabled: undefined,\n label: undefined,\n})\n\nconst ariaLabel = useAriaLabel(() => props.label)\n\ndefineSlots<{\n /** The VButtons and VIconButtons to join together. */\n default(): unknown\n}>()\n\n// Getters, so the group's props stay reactive on the other side of the injection.\nprovide(buttonGroupKey, {\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get elevated() {\n return props.elevated\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst groupEl = ref<HTMLElement | null>(null)\n\n// A template ref on this component is not a safe way to its element: the comment above the\n// root makes the template a fragment in development, where `$el` is a text anchor. VToggle,\n// whose root this group is, reads the element from here.\ndefineExpose({\n /** The `role=\"group\"` element. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n `data-bordered` is withheld under `detached`, so the DOM never carries a claim the markup\n cannot honour: apart, the buttons share no edge for a line to sit on.\n -->\n <div\n ref=\"groupEl\"\n class=\"v-button-group\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-orientation=\"orientation\"\n :data-detached=\"detached ? '' : undefined\"\n :data-bordered=\"bordered && !detached ? '' : undefined\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * `.v-overlay` appears in both guards, and neither is optional. A panel must not pass for a\n * segment: VMenu renders its panel as a SIBLING of its trigger, so a group holding a menu has\n * a child that is no segment at all.\n */\n .v-button-group {\n display: inline-flex;\n /* Gives every segment the same height in a row, and the same width in a column,\n whatever each button's own content measures. */\n align-items: stretch;\n /* Declared on every group although only a raised one ever moves its shadow: that is\n what lets the reduced-motion block cancel it with a bare selector rather than\n repeating the elevation `:has()` below. */\n transition: box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-button-group[data-orientation='vertical'] {\n flex-direction: column;\n }\n\n /* An explicit display alongside the inline size, VButton's own full-width argument: an\n inline-level box sits on a line box, and the strut's descender would show as a few\n pixels of dead space under a row asked to fill its parent. */\n .v-button-group[data-full-width] {\n display: flex;\n inline-size: 100%;\n }\n\n /*\n * Grid tracks equalize complete border boxes; flex distributes content boxes and leaves\n * wrapped segments narrower by their missing padding.\n */\n .v-button-group[data-full-width][data-orientation='horizontal'] {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n }\n\n /*\n * The two selectors are what keeps this out of a plain horizontal row, and that is not an\n * optimization. Nothing is stretched there: each segment is as wide as its own content, so a\n * percentage on the button would resolve against a wrapper whose width depends on that very\n * button, which is circular, and a wrapped segment would stop measuring its label.\n */\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)),\n .v-button-group[data-full-width][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)) {\n inline-size: 100%;\n }\n\n /* Detached, the buttons are simply spaced: the base rule already gives the flex box\n and the vertical direction, so a gap and the cross-axis alignment are the whole\n difference. The vertical rule restores the stretching the base declares, which the\n centring above beats by one attribute. */\n .v-button-group[data-detached] {\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-button-group[data-detached][data-orientation='vertical'] {\n align-items: stretch;\n }\n\n /*\n * The negative margin pulls each segment onto its neighbour so their two 1px borders collapse\n * into one line, over which the seam below is then laid: it draws the separation the filled\n * variants would otherwise lack (their border is transparent) and unifies the joint between\n * two outlined ones. Each block is scoped to one orientation on purpose.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-inline-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n }\n\n /*\n * The first segment needs none of that: a panel is rendered after the trigger it belongs to,\n * so it can never take `:first-child` from a segment.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-block-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-start-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-end-start-radius: 0;\n border-end-end-radius: 0;\n }\n\n /*\n * The seam is a box of its own, and not the neighbour's own border, because CSS joins two\n * adjacent borders at a MITRE: where a 1px border meets the transparent top and bottom ones\n * of a solid segment, its last pixel is cut on the diagonal and the bar ends in a notch at\n * each end. A box carrying that border on ONE side has no second border to join, so its ends\n * stay square.\n */\n .v-button-group > .v-button,\n .v-button-group > :not(:where(.v-overlay, .v-button-group)) .v-button:not(:where(.v-overlay *)) {\n position: relative;\n }\n\n /* Only the generating rule is guarded. The two orientation rules below place the insets\n of a pseudo-element that, unless the row is bordered, is never generated at all. */\n .v-button-group[data-bordered] > .v-button:not(:first-child)::before,\n .v-button-group[data-bordered]\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n content: '';\n position: absolute;\n }\n\n .v-button-group[data-orientation='horizontal'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-block: -1px;\n inset-inline-start: -1px;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n .v-button-group[data-orientation='vertical'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-inline: -1px;\n inset-block-start: -1px;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * Unless the row is bordered, the borders on both sides of every shared edge are cleared. The\n * `:not([data-detached])` guard is also what brings these to (0,6,0), and that weight is not\n * decoration. Without it they are (0,5,0), which TIES with the compound a component overrides\n * a segment's border through\n * (VToggle's outline frame,\n * `.v-toggle[data-item-variant='outline'] > .v-toggle-item[aria-pressed='true']:is(…)`), and\n * a tie between two sheets is settled by whichever the consumer's bundler put last.\n */\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > .v-button:not(:first-child),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-inline-start-color: transparent;\n }\n\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-inline-end-color: transparent;\n }\n\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > .v-button:not(:first-child),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-block-start-color: transparent;\n }\n\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-block-end-color: transparent;\n }\n\n /*\n * Its argument is a list, so a wrapped segment answers for itself, and since a selector list\n * takes the specificity of its heaviest member, the two branches leave the weight where it\n * was. The raised BACKGROUND stays with each button.\n */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated],\n > :not(:where(.v-overlay, .v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *))\n ) {\n border-radius: var(--vectis-radius-interactive);\n box-shadow: var(--vectis-shadow-sm);\n }\n\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-md);\n }\n\n /* After the hover rule and at equal specificity, the VButton order: pressing a\n raised segment settles the row back down. */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-sm);\n }\n\n /*\n * VButton's own hover and active shadows are (0,4,0) on this very element, so at equal\n * specificity the winner would be whichever of the two sheets the consumer's bundler happened\n * to put last.\n */\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated],\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:hover,\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:active,\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:where(.v-overlay *)) {\n box-shadow: none;\n }\n\n /*\n * Raise keyboard focus above the next segment to preserve the shared-edge ring. Do not raise\n * hover/active states, which would cover the neighbour's border seam.\n */\n .v-button-group > .v-button:focus-visible,\n .v-button-group\n > :not(:where(.v-overlay, .v-button-group))\n .v-button:focus-visible:not(:where(.v-overlay *)) {\n z-index: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-button-group {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":""}
@@ -6,7 +6,7 @@ var l = [
6
6
  "aria-label",
7
7
  "data-orientation",
8
8
  "data-detached",
9
- "data-seamless",
9
+ "data-bordered",
10
10
  "data-full-width"
11
11
  ], u = /*@__PURE__*/ r({
12
12
  __name: "VButtonGroup",
@@ -16,7 +16,7 @@ var l = [
16
16
  type: Boolean,
17
17
  default: !1
18
18
  },
19
- seamless: {
19
+ bordered: {
20
20
  type: Boolean,
21
21
  default: !1
22
22
  },
@@ -72,7 +72,7 @@ var l = [
72
72
  "aria-label": c(f),
73
73
  "data-orientation": r.orientation,
74
74
  "data-detached": r.detached ? "" : void 0,
75
- "data-seamless": r.seamless && !r.detached ? "" : void 0,
75
+ "data-bordered": r.bordered && !r.detached ? "" : void 0,
76
76
  "data-full-width": r.fullWidth ? "" : void 0
77
77
  }, [s(e.$slots, "default")], 8, l));
78
78
  }
@@ -1 +1 @@
1
- {"version":3,"file":"VButtonGroup.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VButton/VButtonGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Group shape overrides individual buttons; tone is a fallback and disabled is cumulative. CSS\n * joins borders without changing native activation.\n */\nimport { provide, ref } from 'vue'\n\nimport type { ButtonSize, ButtonTone, ButtonVariant } from './VButton.vue'\nimport { buttonGroupKey } from './context'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\n\n/** Which way the row runs. */\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\n\ninterface ButtonGroupProps {\n /**\n * The direction the buttons are joined in: a row by default, or a column under\n * `vertical`.\n */\n orientation?: ButtonGroupOrientation\n /**\n * Leaves the buttons as separate ones, with a gap between them and each keeping its\n * own corners, instead of joining them into a segmented control.\n */\n detached?: boolean\n /**\n * Takes the lines out from between the joined buttons, so the row reads as one frame rather\n * than as segments.\n */\n seamless?: boolean\n /**\n * Stretches the row across the whole inline size of its parent, every segment taking an equal\n * share of that width.\n */\n fullWidth?: boolean\n /**\n * How much visual weight every segment carries, on the values of VButton's own\n * `variant`: `solid`, `outline`, `ghost` or `soft`. It wins over the variant a\n * button inside was given.\n */\n variant?: ButtonVariant\n /** The colour the segments take, among `accent`, `neutral` and `danger`. */\n tone?: ButtonTone\n /**\n * The height of the segments, from the size scale shared by every control:\n * `xs`, `sm`, `md`, `lg` or `xl`. It wins over the size a button inside was given.\n */\n size?: ButtonSize\n /**\n * Takes 4px off the height of every segment. It wins over the value a button inside\n * was given.\n */\n compact?: boolean\n /**\n * Raises every segment off the page, on the terms of VButton's own `elevated`. It\n * wins over the value a button inside was given.\n */\n elevated?: boolean\n /**\n * Makes every segment unusable. A button that disables itself stays disabled either\n * way: the two answers add up rather than one overruling the other.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row, which is a `role=\"group\"`: \"Text formatting\",\n * \"View\". A consumer `aria-label` or `aria-labelledby` wins over it.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ButtonGroupProps>(), {\n // `detached`, `seamless` and `fullWidth` are the group's own layout, and take a real\n // default like the orientation does. The six that follow travel down to the buttons,\n // and are `undefined` by default, the booleans included: it is `undefined`, and not\n // `false`, that means the group has no opinion and lets the button keep its own.\n orientation: 'horizontal',\n detached: false,\n seamless: false,\n fullWidth: false,\n variant: undefined,\n tone: undefined,\n size: undefined,\n compact: undefined,\n elevated: undefined,\n disabled: undefined,\n label: undefined,\n})\n\nconst ariaLabel = useAriaLabel(() => props.label)\n\ndefineSlots<{\n /** The VButtons and VIconButtons to join together. */\n default(): unknown\n}>()\n\n// Getters, so the group's props stay reactive on the other side of the injection.\nprovide(buttonGroupKey, {\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get elevated() {\n return props.elevated\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst groupEl = ref<HTMLElement | null>(null)\n\n// A template ref on this component is not a safe way to its element: the comment above the\n// root makes the template a fragment in development, where `$el` is a text anchor. VToggle,\n// whose root this group is, reads the element from here.\ndefineExpose({\n /** The `role=\"group\"` element. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n `data-seamless` is withheld under `detached`, so the DOM never carries a claim the markup\n cannot honour: apart, the buttons share no edge for a line to sit on.\n -->\n <div\n ref=\"groupEl\"\n class=\"v-button-group\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-orientation=\"orientation\"\n :data-detached=\"detached ? '' : undefined\"\n :data-seamless=\"seamless && !detached ? '' : undefined\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * `.v-overlay` appears in both guards, and neither is optional. A panel must not pass for a\n * segment: VMenu renders its panel as a SIBLING of its trigger, so a group holding a menu has\n * a child that is no segment at all.\n */\n .v-button-group {\n display: inline-flex;\n /* Gives every segment the same height in a row, and the same width in a column,\n whatever each button's own content measures. */\n align-items: stretch;\n /* Declared on every group although only a raised one ever moves its shadow: that is\n what lets the reduced-motion block cancel it with a bare selector rather than\n repeating the elevation `:has()` below. */\n transition: box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-button-group[data-orientation='vertical'] {\n flex-direction: column;\n }\n\n /* An explicit display alongside the inline size, VButton's own full-width argument: an\n inline-level box sits on a line box, and the strut's descender would show as a few\n pixels of dead space under a row asked to fill its parent. */\n .v-button-group[data-full-width] {\n display: flex;\n inline-size: 100%;\n }\n\n /*\n * Grid tracks equalize complete border boxes; flex distributes content boxes and leaves\n * wrapped segments narrower by their missing padding.\n */\n .v-button-group[data-full-width][data-orientation='horizontal'] {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n }\n\n /*\n * The two selectors are what keeps this out of a plain horizontal row, and that is not an\n * optimization. Nothing is stretched there: each segment is as wide as its own content, so a\n * percentage on the button would resolve against a wrapper whose width depends on that very\n * button, which is circular, and a wrapped segment would stop measuring its label.\n */\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)),\n .v-button-group[data-full-width][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)) {\n inline-size: 100%;\n }\n\n /* Detached, the buttons are simply spaced: the base rule already gives the flex box\n and the vertical direction, so a gap and the cross-axis alignment are the whole\n difference. The vertical rule restores the stretching the base declares, which the\n centring above beats by one attribute. */\n .v-button-group[data-detached] {\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-button-group[data-detached][data-orientation='vertical'] {\n align-items: stretch;\n }\n\n /*\n * The negative margin pulls each segment onto its neighbour so their two 1px borders collapse\n * into one line, over which the seam below is then laid: it draws the separation the filled\n * variants would otherwise lack (their border is transparent) and unifies the joint between\n * two outlined ones. Each block is scoped to one orientation on purpose.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-inline-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n }\n\n /*\n * The first segment needs none of that: a panel is rendered after the trigger it belongs to,\n * so it can never take `:first-child` from a segment.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-block-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-start-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-end-start-radius: 0;\n border-end-end-radius: 0;\n }\n\n /*\n * The seam is a box of its own, and not the neighbour's own border, because CSS joins two\n * adjacent borders at a MITRE: where a 1px border meets the transparent top and bottom ones\n * of a solid segment, its last pixel is cut on the diagonal and the bar ends in a notch at\n * each end. A box carrying that border on ONE side has no second border to join, so its ends\n * stay square.\n */\n .v-button-group > .v-button,\n .v-button-group > :not(:where(.v-overlay, .v-button-group)) .v-button:not(:where(.v-overlay *)) {\n position: relative;\n }\n\n /* Only the generating rule is guarded. The two orientation rules below place the insets\n of a pseudo-element that, apart or seamless, is never generated at all. */\n .v-button-group:not([data-detached]):not([data-seamless]) > .v-button:not(:first-child)::before,\n .v-button-group:not([data-detached]):not([data-seamless])\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n content: '';\n position: absolute;\n }\n\n .v-button-group[data-orientation='horizontal'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-block: -1px;\n inset-inline-start: -1px;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n .v-button-group[data-orientation='vertical'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-inline: -1px;\n inset-block-start: -1px;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * The class is doubled to reach (0,6,0), and that is not decoration. Written once these are\n * (0,5,0), which TIES with the compound a component overrides a segment's border through\n * (VToggle's outline frame,\n * `.v-toggle[data-item-variant='outline'] > .v-toggle-item[aria-pressed='true']:is(…)`), and\n * a tie between two sheets is settled by whichever the consumer's bundler put last.\n */\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > .v-button:not(:first-child),\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-inline-start-color: transparent;\n }\n\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group.v-button-group[data-seamless][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-inline-end-color: transparent;\n }\n\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > .v-button:not(:first-child),\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-block-start-color: transparent;\n }\n\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group.v-button-group[data-seamless][data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-block-end-color: transparent;\n }\n\n /*\n * Its argument is a list, so a wrapped segment answers for itself, and since a selector list\n * takes the specificity of its heaviest member, the two branches leave the weight where it\n * was. The raised BACKGROUND stays with each button.\n */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated],\n > :not(:where(.v-overlay, .v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *))\n ) {\n border-radius: var(--vectis-radius-interactive);\n box-shadow: var(--vectis-shadow-sm);\n }\n\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-md);\n }\n\n /* After the hover rule and at equal specificity, the VButton order: pressing a\n raised segment settles the row back down. */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-sm);\n }\n\n /*\n * VButton's own hover and active shadows are (0,4,0) on this very element, so at equal\n * specificity the winner would be whichever of the two sheets the consumer's bundler happened\n * to put last.\n */\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated],\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:hover,\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:active,\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:where(.v-overlay *)) {\n box-shadow: none;\n }\n\n /*\n * Raise keyboard focus above the next segment to preserve the shared-edge ring. Do not raise\n * hover/active states, which would cover the neighbour's border seam.\n */\n .v-button-group > .v-button:focus-visible,\n .v-button-group\n > :not(:where(.v-overlay, .v-button-group))\n .v-button:focus-visible:not(:where(.v-overlay *)) {\n z-index: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-button-group {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuEA,IAAM,IAAQ,GAkBR,IAAY,QAAmB,EAAM,KAAK;EAQhD,EAAQ,GAAgB;GACtB,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;EACF,CAAC;EAED,IAAM,IAAU,EAAwB,IAAI;SAK5C,EAAa,EAEX,IAAI,EACN,CAAC,mBAQC,EAWM,OAAA;YAVA;GAAJ,KAAI;GACJ,OAAM;GACN,MAAK;GACJ,cAAY,EAAA,CAAA;GACZ,oBAAkB,EAAA;GAClB,iBAAe,EAAA,WAAQ,KAAQ,KAAA;GAC/B,iBAAe,EAAA,YAAQ,CAAK,EAAA,WAAQ,KAAQ,KAAA;GAC5C,mBAAiB,EAAA,YAAS,KAAQ,KAAA;MAEnC,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA"}
1
+ {"version":3,"file":"VButtonGroup.vue_vue_type_script_setup_true_lang.js","names":[],"sources":["../../../src/components/VButton/VButtonGroup.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/**\n * Group shape overrides individual buttons; tone is a fallback and disabled is cumulative. CSS\n * joins borders without changing native activation.\n */\nimport { provide, ref } from 'vue'\n\nimport type { ButtonSize, ButtonTone, ButtonVariant } from './VButton.vue'\nimport { buttonGroupKey } from './context'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\n\n/** Which way the row runs. */\nexport type ButtonGroupOrientation = 'horizontal' | 'vertical'\n\ninterface ButtonGroupProps {\n /**\n * The direction the buttons are joined in: a row by default, or a column under\n * `vertical`.\n */\n orientation?: ButtonGroupOrientation\n /**\n * Leaves the buttons as separate ones, with a gap between them and each keeping its\n * own corners, instead of joining them into a segmented control.\n */\n detached?: boolean\n /**\n * Draws a line between the joined buttons, so the row reads as segments rather than as one\n * frame. It has no effect under `detached`.\n */\n bordered?: boolean\n /**\n * Stretches the row across the whole inline size of its parent, every segment taking an equal\n * share of that width.\n */\n fullWidth?: boolean\n /**\n * How much visual weight every segment carries, on the values of VButton's own\n * `variant`: `solid`, `outline`, `ghost` or `soft`. It wins over the variant a\n * button inside was given.\n */\n variant?: ButtonVariant\n /** The colour the segments take, among `accent`, `neutral` and `danger`. */\n tone?: ButtonTone\n /**\n * The height of the segments, from the size scale shared by every control:\n * `xs`, `sm`, `md`, `lg` or `xl`. It wins over the size a button inside was given.\n */\n size?: ButtonSize\n /**\n * Takes 4px off the height of every segment. It wins over the value a button inside\n * was given.\n */\n compact?: boolean\n /**\n * Raises every segment off the page, on the terms of VButton's own `elevated`. It\n * wins over the value a button inside was given.\n */\n elevated?: boolean\n /**\n * Makes every segment unusable. A button that disables itself stays disabled either\n * way: the two answers add up rather than one overruling the other.\n */\n disabled?: boolean\n /**\n * What screen readers announce for the row, which is a `role=\"group\"`: \"Text formatting\",\n * \"View\". A consumer `aria-label` or `aria-labelledby` wins over it.\n */\n label?: string\n}\n\nconst props = withDefaults(defineProps<ButtonGroupProps>(), {\n // `detached`, `bordered` and `fullWidth` are the group's own layout, and take a real\n // default like the orientation does. The six that follow travel down to the buttons,\n // and are `undefined` by default, the booleans included: it is `undefined`, and not\n // `false`, that means the group has no opinion and lets the button keep its own.\n orientation: 'horizontal',\n detached: false,\n bordered: false,\n fullWidth: false,\n variant: undefined,\n tone: undefined,\n size: undefined,\n compact: undefined,\n elevated: undefined,\n disabled: undefined,\n label: undefined,\n})\n\nconst ariaLabel = useAriaLabel(() => props.label)\n\ndefineSlots<{\n /** The VButtons and VIconButtons to join together. */\n default(): unknown\n}>()\n\n// Getters, so the group's props stay reactive on the other side of the injection.\nprovide(buttonGroupKey, {\n get variant() {\n return props.variant\n },\n get tone() {\n return props.tone\n },\n get size() {\n return props.size\n },\n get compact() {\n return props.compact\n },\n get elevated() {\n return props.elevated\n },\n get disabled() {\n return props.disabled\n },\n})\n\nconst groupEl = ref<HTMLElement | null>(null)\n\n// A template ref on this component is not a safe way to its element: the comment above the\n// root makes the template a fragment in development, where `$el` is a text anchor. VToggle,\n// whose root this group is, reads the element from here.\ndefineExpose({\n /** The `role=\"group\"` element. */\n el: groupEl,\n})\n</script>\n\n<template>\n <!--\n `data-bordered` is withheld under `detached`, so the DOM never carries a claim the markup\n cannot honour: apart, the buttons share no edge for a line to sit on.\n -->\n <div\n ref=\"groupEl\"\n class=\"v-button-group\"\n role=\"group\"\n :aria-label=\"ariaLabel\"\n :data-orientation=\"orientation\"\n :data-detached=\"detached ? '' : undefined\"\n :data-bordered=\"bordered && !detached ? '' : undefined\"\n :data-full-width=\"fullWidth ? '' : undefined\"\n >\n <slot />\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n /*\n * `.v-overlay` appears in both guards, and neither is optional. A panel must not pass for a\n * segment: VMenu renders its panel as a SIBLING of its trigger, so a group holding a menu has\n * a child that is no segment at all.\n */\n .v-button-group {\n display: inline-flex;\n /* Gives every segment the same height in a row, and the same width in a column,\n whatever each button's own content measures. */\n align-items: stretch;\n /* Declared on every group although only a raised one ever moves its shadow: that is\n what lets the reduced-motion block cancel it with a bare selector rather than\n repeating the elevation `:has()` below. */\n transition: box-shadow var(--vectis-duration-fast) var(--vectis-ease-default);\n }\n\n .v-button-group[data-orientation='vertical'] {\n flex-direction: column;\n }\n\n /* An explicit display alongside the inline size, VButton's own full-width argument: an\n inline-level box sits on a line box, and the strut's descender would show as a few\n pixels of dead space under a row asked to fill its parent. */\n .v-button-group[data-full-width] {\n display: flex;\n inline-size: 100%;\n }\n\n /*\n * Grid tracks equalize complete border boxes; flex distributes content boxes and leaves\n * wrapped segments narrower by their missing padding.\n */\n .v-button-group[data-full-width][data-orientation='horizontal'] {\n display: grid;\n grid-auto-flow: column;\n grid-auto-columns: 1fr;\n }\n\n /*\n * The two selectors are what keeps this out of a plain horizontal row, and that is not an\n * optimization. Nothing is stretched there: each segment is as wide as its own content, so a\n * percentage on the button would resolve against a wrapper whose width depends on that very\n * button, which is circular, and a wrapped segment would stop measuring its label.\n */\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)),\n .v-button-group[data-full-width][data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *)))\n :is(.v-button, :has(.v-button)):not(:where(.v-overlay, .v-overlay *)) {\n inline-size: 100%;\n }\n\n /* Detached, the buttons are simply spaced: the base rule already gives the flex box\n and the vertical direction, so a gap and the cross-axis alignment are the whole\n difference. The vertical rule restores the stretching the base declares, which the\n centring above beats by one attribute. */\n .v-button-group[data-detached] {\n align-items: center;\n gap: var(--vectis-space-1);\n }\n\n .v-button-group[data-detached][data-orientation='vertical'] {\n align-items: stretch;\n }\n\n /*\n * The negative margin pulls each segment onto its neighbour so their two 1px borders collapse\n * into one line, over which the seam below is then laid: it draws the separation the filled\n * variants would otherwise lack (their border is transparent) and unifies the joint between\n * two outlined ones. Each block is scoped to one orientation on purpose.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-inline-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='horizontal'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-end-start-radius: 0;\n }\n\n /*\n * The first segment needs none of that: a panel is rendered after the trigger it belongs to,\n * so it can never take `:first-child` from a segment.\n */\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-start-end-radius: 0;\n border-end-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(.v-button:not(:where(.v-overlay *))):not(\n :first-child\n ) {\n margin-block-start: -1px;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical'] > .v-button:not(:first-child),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-start-start-radius: 0;\n border-start-end-radius: 0;\n }\n\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-end-start-radius: 0;\n border-end-end-radius: 0;\n }\n\n /*\n * The seam is a box of its own, and not the neighbour's own border, because CSS joins two\n * adjacent borders at a MITRE: where a 1px border meets the transparent top and bottom ones\n * of a solid segment, its last pixel is cut on the diagonal and the bar ends in a notch at\n * each end. A box carrying that border on ONE side has no second border to join, so its ends\n * stay square.\n */\n .v-button-group > .v-button,\n .v-button-group > :not(:where(.v-overlay, .v-button-group)) .v-button:not(:where(.v-overlay *)) {\n position: relative;\n }\n\n /* Only the generating rule is guarded. The two orientation rules below place the insets\n of a pseudo-element that, unless the row is bordered, is never generated at all. */\n .v-button-group[data-bordered] > .v-button:not(:first-child)::before,\n .v-button-group[data-bordered]\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n content: '';\n position: absolute;\n }\n\n .v-button-group[data-orientation='horizontal'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-block: -1px;\n inset-inline-start: -1px;\n border-inline-start: 1px solid var(--vectis-color-border);\n }\n\n .v-button-group[data-orientation='vertical'] > .v-button:not(:first-child)::before,\n .v-button-group[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *))::before {\n inset-inline: -1px;\n inset-block-start: -1px;\n border-block-start: 1px solid var(--vectis-color-border);\n }\n\n /*\n * Unless the row is bordered, the borders on both sides of every shared edge are cleared. The\n * `:not([data-detached])` guard is also what brings these to (0,6,0), and that weight is not\n * decoration. Without it they are (0,5,0), which TIES with the compound a component overrides\n * a segment's border through\n * (VToggle's outline frame,\n * `.v-toggle[data-item-variant='outline'] > .v-toggle-item[aria-pressed='true']:is(…)`), and\n * a tie between two sheets is settled by whichever the consumer's bundler put last.\n */\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > .v-button:not(:first-child),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-inline-start-color: transparent;\n }\n\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='horizontal']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-inline-end-color: transparent;\n }\n\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > .v-button:not(:first-child),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):not(:first-child)\n .v-button:not(:where(.v-overlay *)) {\n border-block-start-color: transparent;\n }\n\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > .v-button:has(~ :not(.v-overlay)),\n .v-button-group:not([data-detached]):not([data-bordered])[data-orientation='vertical']\n > :not(:where(.v-overlay, .v-button-group)):has(~ :not(.v-overlay))\n .v-button:not(:where(.v-overlay *)) {\n border-block-end-color: transparent;\n }\n\n /*\n * Its argument is a list, so a wrapped segment answers for itself, and since a selector list\n * takes the specificity of its heaviest member, the two branches leave the weight where it\n * was. The raised BACKGROUND stays with each button.\n */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated],\n > :not(:where(.v-overlay, .v-button-group)) .v-button[data-elevated]:not(:where(.v-overlay *))\n ) {\n border-radius: var(--vectis-radius-interactive);\n box-shadow: var(--vectis-shadow-sm);\n }\n\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-md);\n }\n\n /* After the hover rule and at equal specificity, the VButton order: pressing a\n raised segment settles the row back down. */\n .v-button-group:not([data-detached]):has(\n > .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']),\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:disabled, [aria-disabled='true']):not(\n :where(.v-overlay *)\n )\n ) {\n box-shadow: var(--vectis-shadow-sm);\n }\n\n /*\n * VButton's own hover and active shadows are (0,4,0) on this very element, so at equal\n * specificity the winner would be whichever of the two sheets the consumer's bundler happened\n * to put last.\n */\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated],\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:hover,\n .v-button-group.v-button-group:not([data-detached]) > .v-button[data-elevated]:active,\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:hover:not(:where(.v-overlay *)),\n .v-button-group.v-button-group:not([data-detached])\n > :not(:where(.v-overlay, .v-button-group))\n .v-button[data-elevated]:active:not(:where(.v-overlay *)) {\n box-shadow: none;\n }\n\n /*\n * Raise keyboard focus above the next segment to preserve the shared-edge ring. Do not raise\n * hover/active states, which would cover the neighbour's border seam.\n */\n .v-button-group > .v-button:focus-visible,\n .v-button-group\n > :not(:where(.v-overlay, .v-button-group))\n .v-button:focus-visible:not(:where(.v-overlay *)) {\n z-index: 1;\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-button-group {\n transition: none;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAuEA,IAAM,IAAQ,GAkBR,IAAY,QAAmB,EAAM,KAAK;EAQhD,EAAQ,GAAgB;GACtB,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,OAAO;IACT,OAAO,EAAM;GACf;GACA,IAAI,UAAU;IACZ,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;GACA,IAAI,WAAW;IACb,OAAO,EAAM;GACf;EACF,CAAC;EAED,IAAM,IAAU,EAAwB,IAAI;SAK5C,EAAa,EAEX,IAAI,EACN,CAAC,mBAQC,EAWM,OAAA;YAVA;GAAJ,KAAI;GACJ,OAAM;GACN,MAAK;GACJ,cAAY,EAAA,CAAA;GACZ,oBAAkB,EAAA;GAClB,iBAAe,EAAA,WAAQ,KAAQ,KAAA;GAC/B,iBAAe,EAAA,YAAQ,CAAK,EAAA,WAAQ,KAAQ,KAAA;GAC5C,mBAAiB,EAAA,YAAS,KAAQ,KAAA;MAEnC,EAAQ,EAAA,QAAA,SAAA,CAAA,GAAA,GAAA,CAAA"}
@@ -1,2 +1,2 @@
1
1
  @layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
2
- @layer vectis.components{.v-data-table{--data-table-pad-block:var(--vectis-space-3);--data-table-pad-inline:var(--vectis-space-3);--data-table-head-pad-block:var(--vectis-space-2);--data-table-frame-pad:0px;font-family:var(--vectis-text-family);flex-direction:column;block-size:100%;display:flex;container-type:inline-size}.v-data-table[data-compact]{--data-table-pad-block:var(--vectis-space-2);--data-table-pad-inline:var(--vectis-space-2);--data-table-head-pad-block:var(--vectis-space-1)}.v-data-table[data-variant=outlined]{--data-table-frame-pad:var(--data-table-pad-inline);background:var(--vectis-color-surface-raised);border:1px solid var(--vectis-color-border);border-radius:var(--vectis-radius-surface);overflow:clip}.v-data-table-scroller{flex:auto;min-block-size:0;overflow:auto}.v-data-table-toolbar{justify-content:space-between;align-items:center;gap:var(--vectis-space-3);padding-block-start:var(--data-table-frame-pad);padding-block-end:var(--vectis-space-3);padding-inline:var(--data-table-frame-pad);flex-wrap:wrap;flex:none;display:flex}.v-data-table-title{--typography-color:var(--vectis-color-text)}.v-data-table-toolbar .v-input{inline-size:var(--vectis-control-size-table-search);max-inline-size:100%}.v-data-table-table{border-collapse:collapse;width:100%;font-size:var(--vectis-text-body-md-size);color:var(--vectis-color-text)}.v-data-table-caption{padding-block-end:var(--vectis-space-3);padding-inline:var(--data-table-frame-pad);text-align:start;font-size:var(--vectis-text-body-md-size);color:var(--vectis-color-text-muted)}.v-data-table-table th{padding:var(--data-table-head-pad-block) var(--data-table-pad-inline);text-align:start;font-size:var(--vectis-text-body-md-size);font-weight:var(--vectis-font-weight-semibold);color:var(--vectis-color-text-muted);background-color:var(--vectis-color-surface-muted);border-block-end:1px solid var(--vectis-color-border)}.v-data-table-table td{padding:var(--data-table-pad-block) var(--data-table-pad-inline);border-block-end:1px solid var(--vectis-color-border)}.v-data-table-table tbody tr:last-child td{border-block-end:none}.v-data-table-table [data-align=end]{text-align:end}.v-data-table-table [data-align=center]{text-align:center}.v-data-table-stack-label{display:none}.v-data-table-table .v-data-table-select{inline-size:0}.v-data-table[data-striped] tbody tr:nth-child(2n){background-color:var(--vectis-color-surface-sunken)}.v-data-table[data-selectable] tbody tr[data-selected]{background-color:var(--vectis-color-accent-surface)}@media (forced-colors:active){.v-data-table[data-selectable] tbody tr[data-selected]{outline:var(--vectis-focus-ring-width) solid Highlight;outline-offset:calc(-1 * var(--vectis-focus-ring-width))}}.v-data-table[data-sticky-header] th{z-index:1;position:sticky;inset-block-start:0}.v-data-table-sort{--vectis-icon-size:var(--vectis-icon-size-md);--vectis-icon-opsz:20;align-items:center;gap:var(--vectis-space-1);color:inherit;font:inherit;cursor:pointer;border-radius:min(var(--vectis-radius-interactive), .5lh);background:0 0;border:none;padding:0;display:inline-flex}.v-data-table-sort:hover{color:var(--vectis-color-text)}.v-data-table-sort:focus-visible{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-data-table-sort-icon{opacity:.35}.v-data-table-sort[data-direction] .v-data-table-sort-icon{opacity:1}.v-data-table-state{padding:var(--vectis-space-6);text-align:center;color:var(--vectis-color-text-muted)}.v-data-table-state-loading{align-items:center;gap:var(--vectis-space-2);display:inline-flex}.v-data-table-footer{justify-content:flex-end;align-items:center;gap:var(--vectis-space-4);padding-block-start:var(--vectis-space-3);padding-block-end:var(--data-table-frame-pad);padding-inline:var(--data-table-frame-pad);flex-wrap:wrap;flex:none;display:flex}.v-data-table-footer-end{justify-content:flex-end;align-items:center;gap:var(--vectis-space-4);flex-wrap:wrap;margin-inline-start:auto;display:flex}.v-data-table-selection,.v-data-table-range,.v-data-table-per-page-label{font-size:var(--vectis-text-body-md-size);color:var(--vectis-color-text-muted)}.v-data-table-per-page{align-items:center;gap:var(--vectis-space-2);display:flex}@container (width<=640px){.v-data-table[data-responsive=stack] .v-data-table-head{clip-path:inset(50%);width:1px;height:1px;margin:-1px;position:absolute;overflow:hidden}.v-data-table[data-responsive=stack] .v-data-table-head :is(.v-data-table-sort,.v-checkbox){visibility:hidden}.v-data-table[data-responsive=stack] .v-data-table-stack-label{display:inline}.v-data-table[data-responsive=stack] tbody tr{padding-block:var(--vectis-space-2);border-block-end:1px solid var(--vectis-color-border);display:block}.v-data-table[data-responsive=stack] tbody tr:last-child{border-block-end:none}.v-data-table[data-responsive=stack] td{justify-content:space-between;align-items:baseline;gap:var(--vectis-space-4);padding:var(--vectis-space-1) var(--vectis-space-2);text-align:end;border:none;display:flex}.v-data-table[data-responsive=stack] td:before{content:attr(data-label);font-size:var(--vectis-text-overline-size);font-weight:var(--vectis-text-overline-weight);letter-spacing:var(--vectis-text-overline-tracking);color:var(--vectis-color-text-muted)}.v-data-table[data-responsive=stack] td.v-data-table-select{justify-content:flex-start}.v-data-table[data-responsive=stack] td.v-data-table-select:before{content:none}.v-data-table-search{flex:100%}}}
2
+ @layer vectis.components{.v-data-table{--data-table-pad-block:var(--vectis-space-3);--data-table-pad-inline:var(--vectis-space-3);--data-table-head-pad-block:var(--vectis-space-2);--data-table-frame-pad:0px;font-family:var(--vectis-text-family);flex-direction:column;block-size:100%;display:flex}.v-data-table[data-compact]{--data-table-pad-block:var(--vectis-space-2);--data-table-pad-inline:var(--vectis-space-2);--data-table-head-pad-block:var(--vectis-space-1)}.v-data-table[data-variant=outlined]{--data-table-frame-pad:var(--data-table-pad-inline);background:var(--vectis-color-surface-raised);border:1px solid var(--vectis-color-border);border-radius:var(--vectis-radius-surface);overflow:clip}.v-data-table-scroller{flex:auto;min-block-size:0;overflow:auto}.v-data-table-toolbar{justify-content:space-between;align-items:center;gap:var(--vectis-space-3);padding-block-start:var(--data-table-frame-pad);padding-block-end:var(--vectis-space-3);padding-inline:var(--data-table-frame-pad);flex-wrap:wrap;flex:none;display:flex}.v-data-table-title{--typography-color:var(--vectis-color-text)}.v-data-table-toolbar .v-input{inline-size:var(--vectis-control-size-table-search);max-inline-size:100%}.v-data-table-table{border-collapse:collapse;width:100%;font-size:var(--vectis-text-body-md-size);color:var(--vectis-color-text)}.v-data-table-caption{padding-block-end:var(--vectis-space-3);padding-inline:var(--data-table-frame-pad);text-align:start;font-size:var(--vectis-text-body-md-size);color:var(--vectis-color-text-muted)}.v-data-table-table th{padding:var(--data-table-head-pad-block) var(--data-table-pad-inline);text-align:start;font-size:var(--vectis-text-body-md-size);font-weight:var(--vectis-font-weight-semibold);color:var(--vectis-color-text-muted);background-color:var(--vectis-color-surface-muted);border-block-end:1px solid var(--vectis-color-border)}.v-data-table-table td{padding:var(--data-table-pad-block) var(--data-table-pad-inline);border-block-end:1px solid var(--vectis-color-border)}.v-data-table-table tbody tr:last-child td{border-block-end:none}.v-data-table-table [data-align=end]{text-align:end}.v-data-table-table [data-align=center]{text-align:center}.v-data-table-table .v-data-table-select{inline-size:0}.v-data-table[data-striped] tbody tr:nth-child(2n){background-color:var(--vectis-color-surface-sunken)}.v-data-table[data-selectable] tbody tr[data-selected]{background-color:var(--vectis-color-accent-surface)}@media (forced-colors:active){.v-data-table[data-selectable] tbody tr[data-selected]{outline:var(--vectis-focus-ring-width) solid Highlight;outline-offset:calc(-1 * var(--vectis-focus-ring-width))}}.v-data-table[data-sticky-header] th{z-index:1;position:sticky;inset-block-start:0}.v-data-table-sort{--vectis-icon-size:var(--vectis-icon-size-md);--vectis-icon-opsz:20;align-items:center;gap:var(--vectis-space-1);color:inherit;font:inherit;cursor:pointer;border-radius:min(var(--vectis-radius-interactive), .5lh);background:0 0;border:none;padding:0;display:inline-flex}.v-data-table-sort:hover{color:var(--vectis-color-text)}.v-data-table-sort:focus-visible{outline:var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);outline-offset:var(--vectis-focus-ring-offset)}.v-data-table-sort-icon{opacity:.35}.v-data-table-sort[data-direction] .v-data-table-sort-icon{opacity:1}.v-data-table-state{padding:var(--vectis-space-6);text-align:center;color:var(--vectis-color-text-muted)}.v-data-table-state-loading{align-items:center;gap:var(--vectis-space-2);display:inline-flex}.v-data-table-footer{justify-content:flex-end;align-items:center;gap:var(--vectis-space-4);padding-block-start:var(--vectis-space-3);padding-block-end:var(--data-table-frame-pad);padding-inline:var(--data-table-frame-pad);flex-wrap:wrap;flex:none;display:flex}.v-data-table-footer-end{justify-content:flex-end;align-items:center;gap:var(--vectis-space-4);flex-wrap:wrap;margin-inline-start:auto;display:flex}.v-data-table-selection,.v-data-table-range,.v-data-table-per-page-label{font-size:var(--vectis-text-body-md-size);color:var(--vectis-color-text-muted)}.v-data-table-per-page{align-items:center;gap:var(--vectis-space-2);display:flex}}
@@ -70,8 +70,6 @@ export interface DataTableRange {
70
70
  }
71
71
  /** How much decoration the table carries. */
72
72
  export type DataTableVariant = 'flat' | 'outlined';
73
- /** What a narrow container does to the rows. */
74
- export type DataTableResponsive = 'scroll' | 'stack';
75
73
  export interface DataTableProps<Row extends Record<string, unknown>> {
76
74
  /** The columns to show, in order. */
77
75
  columns: DataTableColumn[];
@@ -89,11 +87,6 @@ export interface DataTableProps<Row extends Record<string, unknown>> {
89
87
  * and rounded corners.
90
88
  */
91
89
  variant?: DataTableVariant;
92
- /**
93
- * What happens when the component is too narrow: the table scrolls sideways, or each
94
- * row becomes a card with its column headings repeated inside it.
95
- */
96
- responsive?: DataTableResponsive;
97
90
  /** Shows that the rows are being loaded. */
98
91
  loading?: boolean;
99
92
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"VDataTable.js","names":[],"sources":["../../../src/components/VDataTable/VDataTable.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"Row extends Record<string, unknown>\">\n// @core\n/**\n * Native table semantics remain intact. JavaScript adds sorting, paging, selection and\n * debounced search; browser measurements stay in browser tests.\n */\n\nimport { computed, ref, useId, useSlots, watch, watchEffect } from 'vue'\nimport type { StyleValue } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VCheckbox from '../VCheckbox/VCheckbox.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { arrow_downward as arrowDownwardIcon } from '../VIcon/icons/arrow_downward'\nimport { arrow_drop_down as arrowDropDownIcon } from '../VIcon/icons/arrow_drop_down'\nimport { arrow_upward as arrowUpwardIcon } from '../VIcon/icons/arrow_upward'\nimport { search as searchIcon } from '../VIcon/icons/search'\nimport { swap_vert as swapVertIcon } from '../VIcon/icons/swap_vert'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport VMenu from '../VMenu/VMenu.vue'\nimport VMenuItem from '../VMenu/VMenuItem.vue'\nimport VPagination from '../VPagination/VPagination.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { toggleValue } from '../../utils/array'\nimport { cssSize } from '../../utils/css'\nimport { isDev } from '../../utils/env'\nimport { clamp } from '../../utils/number'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\n\nimport { useTimer } from '../../composables/useTimer'\nimport { useLocale, useMessages } from '../../i18n/state'\n\n/** How a column's content is aligned. */\nexport type DataTableColumnAlign = 'start' | 'center' | 'end'\n\n/** One column of the table. */\nexport interface DataTableColumn {\n /** Which field of a row it shows, and the name its slots are addressed by. */\n key: string\n /** Its heading. */\n label: string\n /** Lets the reader sort by this column. */\n sortable?: boolean\n /** How its content is aligned. Numbers usually belong at the end. */\n align?: DataTableColumnAlign\n}\n\n/** Which way a column is sorted: A to Z, or Z to A. */\nexport type DataTableSortDirection = 'asc' | 'desc'\n\n/** Which column the table is sorted by, and in which direction. */\nexport interface DataTableSort {\n /** The `key` of the column the rows are ordered by. */\n key: string\n /** Which way they are ordered. */\n direction: DataTableSortDirection\n}\n\n/** How a row is identified: by the field named in `rowKey`, or failing that by its position. */\nexport type DataTableRowId = string | number\n\n/**\n * Everything the table is currently being asked for, reported whenever it changes so a\n * server can answer it. What is not set is `null` rather than left out, so the object keeps\n * the same keys once serialized into a request.\n */\nexport interface DataTableParams {\n /** The page asked for, from 1. */\n page: number\n /** How many rows a page holds, `null` when the table is not paginated. */\n perPage: number | null\n /** The `key` of the sorted column, `null` when the rows are not sorted. */\n sortKey: string | null\n /** Which way they are sorted, `null` when they are not. */\n sortDirection: DataTableSortDirection | null\n /** The committed search, empty when nothing is searched for. */\n search: string\n}\n\n/** What the `cell-<key>` slots receive. */\nexport interface DataTableCellSlotProps<Row extends Record<string, unknown>> {\n /** The row the cell belongs to. */\n row: Row\n /** The value of the column's field in that row. */\n value: unknown\n /** The column the cell belongs to. */\n column: DataTableColumn\n}\n\n/** What the `head-<key>` slots receive. */\nexport interface DataTableHeadSlotProps {\n /** The column the heading belongs to. */\n column: DataTableColumn\n}\n\n/** What the `#empty` slot receives. */\nexport interface DataTableEmptySlotProps {\n /** The search that produced the empty result, empty when nothing was searched for. */\n search: string\n}\n\n/** The rows on show, as `rangeText` receives them, counted from 1. */\nexport interface DataTableRange {\n /** The position of the first row on show. */\n start: number\n /** The position of the last row on show. */\n end: number\n /** How many rows there are in all. */\n total: number\n}\n\n/** How much decoration the table carries. */\nexport type DataTableVariant = 'flat' | 'outlined'\n\n/** What a narrow container does to the rows. */\nexport type DataTableResponsive = 'scroll' | 'stack'\n\n// This interface is exported rather than kept local, and it is generic rather than referring to\n// the component's own type parameter. A component typed over its rows inlines the whole\n// signature of its props into the declarations it emits, so a name that is not exported cannot\n// be written there and the build of the type declarations fails.\nexport interface DataTableProps<Row extends Record<string, unknown>> {\n /** The columns to show, in order. */\n columns: DataTableColumn[]\n /** The rows to show. */\n rows: Row[]\n /** Which field identifies a row. */\n rowKey?: string\n /**\n * A sentence describing what the table holds. It is announced before the table itself,\n * and is what tells a screen reader user whether it is worth exploring.\n */\n caption?: string\n /**\n * How the table is framed: nothing at all, or a card with a raised background, a border\n * and rounded corners.\n */\n variant?: DataTableVariant\n /**\n * What happens when the component is too narrow: the table scrolls sideways, or each\n * row becomes a card with its column headings repeated inside it.\n */\n responsive?: DataTableResponsive\n /** Shows that the rows are being loaded. */\n loading?: boolean\n /**\n * What is written beside the spinner while the rows are loading. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /** What is said when there is no row to show. It falls back to the design system dictionary. */\n emptyText?: string\n /** A title above the table, on the left of its toolbar. */\n title?: string\n /** Adds a search field to the toolbar. */\n searchable?: boolean\n /** What that field says while empty. It falls back to the design system dictionary. */\n searchPlaceholder?: string\n /**\n * What screen readers announce for the search field. It falls back to the design\n * system dictionary.\n */\n searchLabel?: string\n /**\n * When a server does the searching, how long to wait after a keystroke before asking\n * it, in milliseconds. Zero asks at once.\n */\n searchDebounce?: number\n /** Tints every other row, which helps the eye follow a long line across the table. */\n striped?: boolean\n /**\n * Keeps the column headings in place while the rows scroll under them. It needs a\n * bounded scrolling area to work: either the `height` prop, or a parent with a height\n * of its own.\n */\n stickyHeader?: boolean\n /** Tightens the cells by one step, and everything the table renders with them. */\n compact?: boolean\n /**\n * The height of the whole component, toolbar and pagination included: a number is read as\n * pixels, anything else as a CSS length. Left out, the table takes its parent's height\n * whenever the parent has one.\n */\n height?: number | string\n /** The heading icon of a column that can be sorted but currently is not. */\n sortIcon?: IconSource\n /**\n * The icon of an ascending sort. It points DOWN by default, the spreadsheet\n * convention: sorting A to Z reads downwards.\n */\n sortAscIcon?: IconSource\n /** The icon of a descending sort. */\n sortDescIcon?: IconSource\n /** The choices offered for how many rows a page holds. */\n perPageOptions?: number[]\n /** What that choice is called. It falls back to the design system dictionary. */\n perPageText?: string\n /**\n * How many rows there are in all on the server. It is what lets the pagination and the\n * range be right when the table only ever holds one page.\n */\n total?: number\n /** Shows which rows are being looked at (\"1–10 of 42\") in the footer. */\n showRange?: boolean\n /** Rephrases that range. It falls back to the design system dictionary. */\n rangeText?: (range: DataTableRange) => string\n /** Adds a checkbox to every row, and one in the heading to take the whole page. */\n selectable?: boolean\n /**\n * What the heading checkbox is announced as. It falls back to the design system\n * dictionary.\n */\n selectAllLabel?: string\n /**\n * How the selection is summed up in the footer. It says nothing at all when nothing is\n * selected, and falls back to the design system dictionary.\n */\n selectionText?: (count: number) => string\n /**\n * What a row's checkbox is announced as. \"Select row\" tells a screen reader user nothing\n * about WHICH row, so this is worth supplying with something from the row itself. It falls\n * back to the design system dictionary, which numbers the rows from 1.\n */\n selectRowLabel?: (row: Row, index: number) => string\n /**\n * Hands the searching, the sorting and the paging over to a server: the rows are shown\n * exactly as they arrive, and every change of what is being asked for is reported so\n * the server can answer it.\n */\n serverSide?: boolean\n}\n\nconst props = withDefaults(defineProps<DataTableProps<Row>>(), {\n rowKey: undefined,\n caption: undefined,\n variant: 'flat',\n responsive: 'scroll',\n loading: false,\n loadingText: undefined,\n emptyText: undefined,\n title: undefined,\n searchable: false,\n searchPlaceholder: undefined,\n searchLabel: undefined,\n searchDebounce: 250,\n striped: false,\n stickyHeader: false,\n compact: false,\n height: undefined,\n sortIcon: () => swapVertIcon,\n sortAscIcon: () => arrowDownwardIcon,\n sortDescIcon: () => arrowUpwardIcon,\n perPageOptions: undefined,\n perPageText: undefined,\n total: undefined,\n showRange: false,\n rangeText: undefined,\n selectable: false,\n selectAllLabel: undefined,\n selectionText: undefined,\n selectRowLabel: undefined,\n serverSide: false,\n})\n\nconst m = useMessages()\nconst vectisLocale = useLocale()\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.dataTable.empty)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.dataTable.loading)\nconst resolvedSearchPlaceholder = computed(\n () => props.searchPlaceholder ?? m.value.dataTable.searchPlaceholder,\n)\nconst resolvedSearchLabel = computed(() => props.searchLabel ?? m.value.dataTable.searchLabel)\nconst resolvedPerPageText = computed(() => props.perPageText ?? m.value.dataTable.perPage)\nconst resolvedSelectAllLabel = computed(() => props.selectAllLabel ?? m.value.dataTable.selectAll)\n\n/** Which column the rows are sorted by, and in which direction. */\nconst sort = defineModel<DataTableSort | null>('sort', { default: null })\n/** The page being shown, counted from 1. */\nconst page = defineModel<number>('page', { default: 1 })\n/**\n * How many rows a page holds. Any value above zero turns the pagination on, so passing\n * one down without binding it is enough to enable it.\n */\nconst perPage = defineModel<number | undefined>('perPage', { default: undefined })\n/*\n * `sort` is `null` because the TABLE writes that state itself, a third click on a heading\n * clearing the order, and a model the component writes needs a value a consumer can store and\n * compare. `perPage` is `undefined` because nothing here ever clears it: it is simply a model\n * that was not given.\n */\n/** The selected rows, as the identities `rowKey` gives them, never the row objects themselves. */\nconst selected = defineModel<DataTableRowId[]>('selected', { default: () => [] })\n/**\n * What is typed in the search field, empty to begin with. Only the declared columns are\n * searched, accent- and case-insensitively; in `serverSide` mode nothing is filtered here\n * and the term is reported through `update:params` instead.\n */\nconst search = defineModel<string>('search', { default: '' })\n\nconst emit = defineEmits<{\n /**\n * What the table is now being asked for, when a server is answering: the page, the page size,\n * the sort or the search has changed, the last one after its delay.\n */\n 'update:params': [params: DataTableParams]\n}>()\n\ndefineSlots<{\n /** What a cell of a given column shows: a slot named after that column's key. */\n [name: `cell-${string}`]: (scope: DataTableCellSlotProps<Row>) => unknown\n /** What a column's heading shows: a slot named after that column's key. */\n [name: `head-${string}`]: (scope: DataTableHeadSlotProps) => unknown\n /** The left side of the toolbar, replacing the `title` prop. */\n title?(): unknown\n /** What the table shows while its rows are loading, replacing the spinner and its text. */\n loading?(): unknown\n /**\n * What the table shows when there is no row to show, replacing `emptyText`. It receives the\n * search that produced the empty result, empty when nothing was searched for.\n */\n empty?(scope: DataTableEmptySlotProps): unknown\n}>()\n\n// `class` and `style` stay on the wrapper, where a consumer expects to place the component;\n// everything else; an id, the aria-*; goes to the `<table>` itself, the only element they\n// validly describe.\ndefineOptions({ inheritAttrs: false })\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\nconst slots = useSlots()\n\n// A template ref on the component reaches the wrapper, a layout box; the `<table>` is where\n// the consumer's attributes land and what they describe.\nconst tableEl = ref<HTMLTableElement | null>(null)\ndefineExpose({\n /** The `<table>` element, where the consumer's attributes land. */\n el: tableEl,\n})\n\n// @a11y\n/*\n * A caption already does, and so does a consumer's own `aria-label` or `aria-labelledby`, which\n * a reference to the title would override. Bound before the forwarded attributes, like every\n * state the component owns.\n */\nconst titleId = useId()\nconst tableLabelledBy = computed(() =>\n props.caption ||\n !(props.title || slots.title) ||\n attrs['aria-label'] !== undefined ||\n attrs['aria-labelledby'] !== undefined\n ? undefined\n : titleId,\n)\n\n// @devwarn\nif (isDev) {\n if (props.selectable && !props.rowKey)\n console.warn(\n '[VDataTable] `selectable` without `rowKey` — index-based identities are corrupted by sorting, filtering and pagination.',\n )\n /*\n * A key that is missing, or not a string or a number, turns into the same text on every\n * row (\"undefined\"), and two rows sharing a key share an identity: ticking one ticks them\n * all, and the rows' own `:key`s collide. Once per instance and per kind.\n */\n const warned = new Set<string>()\n watchEffect(() => {\n const key = props.rowKey\n if (!key) return\n const seen = new Set<unknown>()\n for (const row of props.rows) {\n const value = row[key]\n if (typeof value !== 'string' && typeof value !== 'number') {\n if (!warned.has('type')) {\n warned.add('type')\n console.warn(\n `[VDataTable] \\`rowKey\\` \"${key}\" is missing or is not a string or a number on some rows: those rows share one identity.`,\n )\n }\n } else if (seen.has(value)) {\n if (!warned.has('duplicate')) {\n warned.add('duplicate')\n console.warn(\n `[VDataTable] \\`rowKey\\` \"${key}\" holds the value \"${value}\" twice: rows are told apart by it, so it must be unique.`,\n )\n }\n } else seen.add(value)\n }\n })\n}\n\n/*\n * A key that is already a string or a number is returned AS IT IS. The selection holds these\n * identities and is looked up with a Set, which tells the number 7 from the string \"7\": turned\n * into text, a table keyed by numeric ids never found a selected row again, so every checkbox\n * stayed unticked.\n */\nfunction rowIdentity(row: Row, index: number): DataTableRowId {\n if (!props.rowKey) return index\n const key = row[props.rowKey]\n return typeof key === 'string' || typeof key === 'number' ? key : String(key)\n}\n\n// Searching fields the reader cannot see would return rows for reasons nothing on screen\n// explains.\n/*\n * The filter below re-reads the whole table on every keystroke: ten thousand rows across five\n * columns is fifty thousand normalizations per character typed without it.\n */\nconst normalizedOf = createNormalizedCache<Row>()\nconst normalizedCell = (row: Row, key: string) => normalizedOf(row, String(row[key] ?? ''), key)\n\nconst filteredRows = computed(() => {\n if (props.serverSide || !props.searchable) return props.rows\n const needle = normalizeText((search.value ?? '').trim())\n if (!needle) return props.rows\n return props.rows.filter((row) =>\n props.columns.some((column) => normalizedCell(row, column.key).includes(needle)),\n )\n})\n\n/*\n * The locale must nonetheless stay EXPLICIT: an `undefined` one resolves differently in Node\n * and in the browser, so the row order would diverge across hydration.\n */\nconst collator = computed(() => new Intl.Collator(vectisLocale.value, { numeric: true }))\n\n/*\n * What a cell is compared by. A `Date` by its instant: as text it collates on `toString()`,\n * which opens with the WEEKDAY, so Monday the 5th sorted before Saturday the 3rd.\n */\nconst NUMERIC_TEXT = /^\\s*[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?\\s*$/i\nfunction sortKey(value: unknown): unknown {\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'string' && NUMERIC_TEXT.test(value)) return Number(value)\n return value\n}\n\nconst sortedRows = computed(() => {\n const current = sort.value\n if (!current || props.serverSide) return filteredRows.value\n const factor = current.direction === 'asc' ? 1 : -1\n const compare = collator.value.compare\n return [...filteredRows.value].sort((a, b) => {\n const av = sortKey(a[current.key])\n const bv = sortKey(b[current.key])\n if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * factor\n return compare(String(av ?? ''), String(bv ?? '')) * factor\n })\n})\n\nconst paginated = computed(() => (perPage.value ?? 0) > 0)\nconst totalCount = computed(() =>\n props.serverSide ? (props.total ?? props.rows.length) : filteredRows.value.length,\n)\nconst pageCount = computed(() =>\n paginated.value ? Math.max(1, Math.ceil(totalCount.value / (perPage.value ?? 1))) : 1,\n)\n// The page actually shown is CLAMPED by derivation and the model is never rewritten: a\n// search that leaves fewer pages simply falls back to the last one, and typing a\n// character that would have emptied the table does not silently move the consumer's own\n// page number.\nconst currentPage = computed(() => clamp(page.value, 1, pageCount.value))\n\nconst displayedRows = computed(() => {\n // With a server answering, the rows received ARE the current page: cutting them again\n // would show a tenth of a page.\n if (props.serverSide || !paginated.value) return sortedRows.value\n const start = (currentPage.value - 1) * (perPage.value ?? 0)\n return sortedRows.value.slice(start, start + (perPage.value ?? 0))\n})\n\nfunction toggleSort(key: string) {\n const current = sort.value\n if (!current || current.key !== key) sort.value = { key, direction: 'asc' }\n else if (current.direction === 'asc') sort.value = { key, direction: 'desc' }\n else sort.value = null\n}\n\n// @a11y\n// Only the column actually sorted carries the attribute. Setting it on every heading would have a screen reader announce \"not sorted\" on each one in turn, drowning\n// the single piece of information that matters: which column the order comes from.\nfunction ariaSort(column: DataTableColumn): 'ascending' | 'descending' | undefined {\n if (sort.value?.key !== column.key) return undefined\n return sort.value.direction === 'asc' ? 'ascending' : 'descending'\n}\n\n/**\n * Which icon a column's heading shows: a neutral one on any sortable column, and the\n * arrow of the current direction on the one being sorted by.\n *\n * It is deliberately named differently from the prop it falls back to: written the same,\n * the function would shadow the prop inside the template and the icons would never be\n * overridable.\n */\nfunction sortIconFor(column: DataTableColumn): IconSource {\n if (sort.value?.key !== column.key) return props.sortIcon\n return sort.value.direction === 'asc' ? props.sortAscIcon : props.sortDescIcon\n}\n\n// And when a server answers, nothing native waits before asking it, so that has to be written.\n// The waiting is delegated to `useTimer`; a delay of zero running at once, cancellation when\n// the component goes away; and the term that was actually committed is what the parameters\n// below report.\nconst committedSearch = ref(search.value)\nconst searchTimer = useTimer()\n\nfunction commitSearch() {\n // A term typed and erased inside the debounce is no new search: going back to page 1\n // and asking the server again would be a request for what is already on screen.\n if (search.value === committedSearch.value) return\n committedSearch.value = search.value\n page.value = 1\n}\n\nwatch(search, () => {\n if (!props.serverSide) {\n page.value = 1\n return\n }\n searchTimer.start(commitSearch, props.searchDebounce)\n})\n\nconst emptySearch = computed(() => (props.serverSide ? committedSearch.value : search.value))\n\n/*\n * `page` is the model, never the clamped `currentPage`. The clamp reads `total`, which is the\n * server's own answer: read here, a response that changed the total would move the parameters\n * and ask for the page again, and a consumer clearing `total` while it loads would send the\n * reader back to page 1.\n */\n// Committing a search and going back to the first page happen one after the other, but both\n// land in the same flush, so this runs once on the final values; never two requests for what\n// the reader experienced as a single action. Compared by value: a parent handing down an equal\n// `sort` object is no new request.\nwatch(\n [\n page,\n () => perPage.value ?? null,\n () => sort.value?.key ?? null,\n () => sort.value?.direction ?? null,\n committedSearch,\n ],\n ([page, perPage, sortKey, sortDirection, search]) => {\n if (props.serverSide) emit('update:params', { page, perPage, sortKey, sortDirection, search })\n },\n)\n\n// The selection. The heading checkbox covers the rows currently VISIBLE and not the whole\n// table: a box that silently selected forty thousand rows would be a trap.\nconst selectedSet = computed(() => new Set<DataTableRowId>(selected.value))\nconst visibleIds = computed(() => displayedRows.value.map((row, index) => rowIdentity(row, index)))\nconst allVisibleSelected = computed(\n () => visibleIds.value.length > 0 && visibleIds.value.every((id) => selectedSet.value.has(id)),\n)\nconst masterIndeterminate = computed(\n () => !allVisibleSelected.value && visibleIds.value.some((id) => selectedSet.value.has(id)),\n)\n\n/*\n * A row is asked about by the identity `visibleIds` already holds for it: the template reads\n * `visibleIds[index]` for the key, the tint and the checkbox alike, rather than deriving it\n * again three or four times per row per render.\n */\nconst isSelected = (id: DataTableRowId) => selectedSet.value.has(id)\n\nfunction toggleRow(id: DataTableRowId) {\n selected.value = toggleValue(selected.value, id)\n}\n\n// It only ever touches the rows on screen, so what was selected on the other pages\n// survives moving between them.\n// Refused with no row on screen, or while the rows on screen are the PREVIOUS ones behind\n// the loading state: ticked there, it stayed ticked with nothing selected, or selected rows\n// the reader could not see.\nconst masterDisabled = computed(() => visibleIds.value.length === 0 || props.loading)\n\nfunction toggleMaster() {\n if (masterDisabled.value) return\n const ids = visibleIds.value\n if (allVisibleSelected.value) {\n const visible = new Set(ids)\n selected.value = selected.value.filter((id) => !visible.has(id))\n } else {\n const current = selectedSet.value\n selected.value = [...selected.value, ...ids.filter((id) => !current.has(id))]\n }\n}\n\n// @a11y\n// Without this every checkbox in the column would be announced identically, and a screen\n// reader user would have no way of knowing which row they were about to select.\nfunction rowSelectLabel(row: Row, index: number): string {\n // The position in the whole table, or every page would have its own \"Select row 1\". The\n // dictionary counts from one as a human does, the prop from zero as code does.\n const position = (paginated.value ? (currentPage.value - 1) * (perPage.value ?? 0) : 0) + index\n return props.selectRowLabel?.(row, position) ?? m.value.dataTable.selectRow(position + 1)\n}\n\nwatch(perPage, () => {\n page.value = 1\n})\n\nconst colCount = computed(() => props.columns.length + (props.selectable ? 1 : 0))\n\n// @a11y\n/**\n * It is rendered, empty, as soon as rows can be selected at all. A region meant to announce its\n * own changes must already exist before the first change: inserted into the page at the same\n * moment as its text, it announces nothing, and the first row selected would pass in silence.\n */\nconst selectionSummary = computed(() => {\n const count = selected.value.length\n if (count === 0) return ''\n if (props.selectionText) return props.selectionText(count)\n return m.value.dataTable.selection(count)\n})\n\nconst rangeSummary = computed(() => {\n const per = perPage.value ?? 0\n const total = totalCount.value\n const start = total === 0 ? 0 : (currentPage.value - 1) * per + 1\n const end = Math.min(currentPage.value * per, total)\n return props.rangeText?.({ start, end, total }) ?? m.value.dataTable.range({ start, end, total })\n})\n\n// The height is applied to the whole component and not as a ceiling on the scrolling area. A\n// soft ceiling remains available to the consumer through a maximum height of their own: the\n// component is a column whose scrolling area can be compressed.\nconst heightStyle = computed<StyleValue | undefined>(() =>\n props.height !== undefined ? { blockSize: cssSize(props.height) } : undefined,\n)\n</script>\n\n<template>\n <div\n class=\"v-data-table\"\n :class=\"rootClass\"\n :style=\"[heightStyle, rootStyle]\"\n :data-variant=\"variant\"\n :data-responsive=\"responsive\"\n :data-striped=\"striped ? '' : undefined\"\n :data-compact=\"compact ? '' : undefined\"\n :data-sticky-header=\"stickyHeader ? '' : undefined\"\n :data-selectable=\"selectable ? '' : undefined\"\n >\n <div v-if=\"title || $slots.title || searchable\" class=\"v-data-table-toolbar\">\n <VTypography :id=\"titleId\" as=\"div\" variant=\"heading-4\" class=\"v-data-table-title\">\n <slot name=\"title\">{{ title }}</slot>\n </VTypography>\n <VInput\n v-if=\"searchable\"\n v-model=\"search\"\n class=\"v-data-table-search\"\n type=\"search\"\n size=\"sm\"\n :compact=\"compact\"\n :icon-start=\"searchIcon\"\n clearable\n :placeholder=\"resolvedSearchPlaceholder\"\n :aria-label=\"resolvedSearchLabel\"\n />\n </div>\n\n <div class=\"v-data-table-scroller\">\n <table\n ref=\"tableEl\"\n class=\"v-data-table-table\"\n :aria-labelledby=\"tableLabelledBy\"\n v-bind=\"forwardedAttrs\"\n >\n <caption v-if=\"caption\" class=\"v-data-table-caption\">\n {{\n caption\n }}\n </caption>\n <thead class=\"v-data-table-head\">\n <tr>\n <th v-if=\"selectable\" scope=\"col\" class=\"v-data-table-select\">\n <VCheckbox\n :model-value=\"allVisibleSelected\"\n :indeterminate=\"masterIndeterminate\"\n :disabled=\"masterDisabled\"\n :aria-label=\"resolvedSelectAllLabel\"\n @update:model-value=\"toggleMaster\"\n />\n <!--\n Its hidden box would leave the heading empty in the stacked layout (axe\n empty-table-header), so it keeps its name as text there.\n -->\n <span class=\"v-data-table-stack-label\">{{ resolvedSelectAllLabel }}</span>\n </th>\n <th\n v-for=\"column in columns\"\n :key=\"column.key\"\n scope=\"col\"\n :data-align=\"column.align\"\n :aria-sort=\"ariaSort(column)\"\n >\n <button\n v-if=\"column.sortable\"\n type=\"button\"\n class=\"v-data-table-sort\"\n :data-direction=\"sort?.key === column.key ? sort.direction : undefined\"\n @click=\"toggleSort(column.key)\"\n >\n <slot :name=\"`head-${column.key}`\" :column=\"column\">{{ column.label }}</slot>\n <!-- Decorative, and deliberately so: given no label, an icon hides itself\n from screen readers. What the sort state is, is already carried by\n the heading itself. -->\n <VIcon class=\"v-data-table-sort-icon\" v-bind=\"iconProps(sortIconFor(column))\" />\n </button>\n <!-- The heading as plain text, shown to screen readers in the STACKED layout\n only, where the button above is hidden: see the stack rules. -->\n <span v-if=\"column.sortable\" class=\"v-data-table-stack-label\">{{\n column.label\n }}</span>\n <template v-else>\n <slot :name=\"`head-${column.key}`\" :column=\"column\">{{ column.label }}</slot>\n </template>\n </th>\n </tr>\n </thead>\n <tbody>\n <!--\n The order matters: loading is checked before emptiness, so a table waiting for its\n rows never claims there are none.\n -->\n <tr v-if=\"loading\">\n <td :colspan=\"colCount\" class=\"v-data-table-state\">\n <slot name=\"loading\">\n <!-- The spinner carries the text for a screen reader, so the visible copy is\n hidden from it: read twice, the cell would say it is loading twice. -->\n <span class=\"v-data-table-state-loading\">\n <VSpinner :label=\"resolvedLoadingText\" />\n <span aria-hidden=\"true\">{{ resolvedLoadingText }}</span>\n </span>\n </slot>\n </td>\n </tr>\n <tr v-else-if=\"displayedRows.length === 0\">\n <td :colspan=\"colCount\" class=\"v-data-table-state\">\n <slot name=\"empty\" :search=\"emptySearch\">{{ resolvedEmptyText }}</slot>\n </td>\n </tr>\n <template v-else>\n <tr\n v-for=\"(row, index) in displayedRows\"\n :key=\"visibleIds[index]\"\n :data-selected=\"selectable && isSelected(visibleIds[index]!) ? '' : undefined\"\n >\n <!--\n The selection is marked with a plain attribute and not with the ARIA selected\n state, which is invalid on the row of a table: it belongs to a grid. What tells\n assistive technology that a row is selected is its checkbox being checked.\n -->\n <td v-if=\"selectable\" class=\"v-data-table-select\">\n <VCheckbox\n :model-value=\"isSelected(visibleIds[index]!)\"\n :aria-label=\"rowSelectLabel(row, index)\"\n @update:model-value=\"toggleRow(visibleIds[index]!)\"\n />\n </td>\n <td\n v-for=\"column in columns\"\n :key=\"column.key\"\n :data-label=\"column.label\"\n :data-align=\"column.align\"\n >\n <slot\n :name=\"`cell-${column.key}`\"\n :row=\"row\"\n :value=\"row[column.key]\"\n :column=\"column\"\n >\n {{ row[column.key] }}\n </slot>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n\n <!-- The footer has two zones: what is selected on the left, and on the right the page\n size, the range and the pagination, in that order. -->\n <div v-if=\"paginated || selectable\" class=\"v-data-table-footer\">\n <span v-if=\"selectable\" class=\"v-data-table-selection\" aria-live=\"polite\">{{\n selectionSummary\n }}</span>\n <div v-if=\"paginated\" class=\"v-data-table-footer-end\">\n <div v-if=\"perPageOptions?.length\" class=\"v-data-table-per-page\">\n <span class=\"v-data-table-per-page-label\" aria-hidden=\"true\">{{\n resolvedPerPageText\n }}</span>\n <!-- The panel is told to match its trigger, which here replaces the default\n minimum width with something sensible: a menu of \"10\", \"25\", \"50\" has no\n use for the width a menu of commands assumes, and it still cannot end up\n narrower than the button opening it. -->\n <VMenu size=\"sm\" :compact=\"compact\" placement=\"top-end\" match-trigger>\n <template #trigger=\"{ triggerProps }\">\n <VButton\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n :compact=\"compact\"\n v-bind=\"triggerProps\"\n :aria-label=\"m.dataTable.perPageValue(resolvedPerPageText, perPage ?? 0)\"\n >\n {{ perPage }}\n <VIcon :name=\"arrowDropDownIcon\" />\n </VButton>\n </template>\n <VMenuItem\n v-for=\"option in perPageOptions\"\n :key=\"option\"\n :label=\"String(option)\"\n :selected=\"option === perPage\"\n @select=\"perPage = option\"\n />\n </VMenu>\n </div>\n <span v-if=\"showRange\" class=\"v-data-table-range\" aria-live=\"polite\">{{\n rangeSummary\n }}</span>\n <!--\n Named after the table rather than with the generic pagination wording: a page holding\n this table and a pagination of its own would otherwise expose two navigation landmarks\n with the same name, and a screen reader user could not tell them apart.\n -->\n <VPagination\n v-model=\"page\"\n :label=\"m.dataTable.pagination\"\n :length=\"pageCount\"\n size=\"sm\"\n :compact=\"compact\"\n align=\"end\"\n :total-visible=\"7\"\n />\n </div>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-data-table {\n /*\n * It does not go through the shared control scale; there is no single control height in a\n * table; which is the same case VAccordion is in.\n */\n --data-table-pad-block: var(--vectis-space-3);\n --data-table-pad-inline: var(--vectis-space-3);\n --data-table-head-pad-block: var(--vectis-space-2);\n\n /* The gutter between the frame and what it holds: nothing when the table is\n unframed, so the caption, the toolbar and the footer sit flush with the edge, and\n the cells' own inline padding as soon as a frame appears. */\n --data-table-frame-pad: 0px;\n\n container-type: inline-size;\n font-family: var(--vectis-text-family);\n\n /*\n * Deliberately no `min-block-size: 0` here. Inside a flex column parent too short for it,\n * the automatic minimum is what keeps the table at its natural height instead of letting it\n * be crushed to nothing.\n */\n display: flex;\n flex-direction: column;\n block-size: 100%;\n }\n\n .v-data-table[data-compact] {\n --data-table-pad-block: var(--vectis-space-2);\n --data-table-pad-inline: var(--vectis-space-2);\n --data-table-head-pad-block: var(--vectis-space-1);\n }\n\n /*\n * The card. The unframed default has nothing to undo, since it declares no decoration at all;\n * whatever surrounds the table is what provides the surface then.\n */\n .v-data-table[data-variant='outlined'] {\n --data-table-frame-pad: var(--data-table-pad-inline);\n\n background: var(--vectis-color-surface-raised);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-surface);\n /*\n * `clip` and not `hidden`. Hiding the overflow would make this element a scroll container\n * of its own, and the sticky heading would then anchor to IT rather than to the area that\n * actually scrolls; which is to say it would not stick at all.\n */\n overflow: clip;\n }\n\n /*\n * `min-block-size: 0` is load-bearing; a flex item refuses by default to shrink below its\n * content, so without it the area never compresses and the table overflows instead of\n * scrolling. Deliberately not confined to `data-responsive='scroll'`: a table taller than its\n * host must scroll in `stack` too, or `outlined`'s clip crops it and the rows below become\n * unreachable.\n */\n .v-data-table-scroller {\n flex: 1 1 auto;\n min-block-size: 0;\n overflow: auto;\n }\n\n .v-data-table-toolbar {\n flex: none;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--vectis-space-3);\n padding-block-start: var(--data-table-frame-pad);\n padding-block-end: var(--vectis-space-3);\n padding-inline: var(--data-table-frame-pad);\n }\n\n /*\n * It is set through the colour VARIABLE that component reads, and not with a plain colour\n * declaration. That declaration would collide with VTypography's own at equal specificity,\n * and the winner would be decided by whichever sheet the consumer's bundler put last.\n */\n .v-data-table-title {\n --typography-color: var(--vectis-color-text);\n }\n\n /* The search field is given a width of its own, overriding the full width VInput takes\n by default. The selector is qualified by its context, which makes it one step more\n specific than VInput's own rule and therefore independent of the order the two sheets\n end up in. */\n .v-data-table-toolbar .v-input {\n inline-size: var(--vectis-control-size-table-search);\n max-inline-size: 100%;\n }\n\n .v-data-table-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--vectis-text-body-md-size);\n color: var(--vectis-color-text);\n }\n\n .v-data-table-caption {\n padding-block-end: var(--vectis-space-3);\n padding-inline: var(--data-table-frame-pad);\n text-align: start;\n font-size: var(--vectis-text-body-md-size);\n color: var(--vectis-color-text-muted);\n }\n\n .v-data-table-table th {\n padding: var(--data-table-head-pad-block) var(--data-table-pad-inline);\n text-align: start;\n font-size: var(--vectis-text-body-md-size);\n /*\n * The heavier weight distinguishes a heading from the data under it, which is emphasis\n * rather than a typographic role; hence a font token read directly.\n */\n font-weight: var(--vectis-font-weight-semibold);\n color: var(--vectis-color-text-muted);\n /*\n * The heading row gets a tint of its own so it reads apart from the data. `muted` and not\n * `sunken`: the striped rows already take `sunken`, and a heading painted the same would\n * read as one more stripe.\n */\n background-color: var(--vectis-color-surface-muted);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-data-table-table td {\n padding: var(--data-table-pad-block) var(--data-table-pad-inline);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-data-table-table tbody tr:last-child td {\n border-block-end: none;\n }\n\n .v-data-table-table [data-align='end'] {\n text-align: end;\n }\n\n .v-data-table-table [data-align='center'] {\n text-align: center;\n }\n\n /* The checkbox column is reduced to the width of its content. A table lays its columns\n out automatically, so asking for no width at all is what makes it take the least\n possible. */\n .v-data-table-stack-label {\n display: none;\n }\n\n .v-data-table-table .v-data-table-select {\n inline-size: 0;\n }\n\n .v-data-table[data-striped] tbody tr:nth-child(even) {\n background-color: var(--vectis-color-surface-sunken);\n }\n\n /* Placed after the striping on purpose: the specificity is the same, so it is the order\n that makes a selected row keep its tint on both odd and even rows. */\n .v-data-table[data-selectable] tbody tr[data-selected] {\n background-color: var(--vectis-color-accent-surface);\n }\n\n /* Forced colours flatten both tints to `Canvas`: the stripes are only a reading aid, but\n a selected row would be told apart by its checkbox alone. It draws an inward outline in\n the system selection colour, which the mode keeps. */\n @media (forced-colors: active) {\n .v-data-table[data-selectable] tbody tr[data-selected] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n }\n\n /*\n * A frozen heading relies on the opaque background every `th` already carries: the rows\n * scroll underneath it, and a consumer restyling that background with a translucent colour\n * will see them through.\n */\n .v-data-table[data-sticky-header] th {\n position: sticky;\n inset-block-start: 0;\n z-index: 1;\n }\n\n .v-data-table-sort {\n /*\n * The icon context for the sort glyph. Without it the icon would fall back to one em; the\n * heading's own text size; and come out visibly smaller than every other icon in the\n * component.\n */\n --vectis-icon-size: var(--vectis-icon-size-md);\n --vectis-icon-opsz: 20;\n\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-1);\n border: none;\n background: transparent;\n padding: 0;\n color: inherit;\n font: inherit;\n cursor: pointer;\n /* Seen only on the focus ring: the control radius capped at half a line, VBreadcrumb's\n link recipe, since a heading that wraps would otherwise round to half its height. */\n border-radius: min(var(--vectis-radius-interactive), 0.5lh);\n }\n\n .v-data-table-sort:hover {\n color: var(--vectis-color-text);\n }\n\n .v-data-table-sort:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* The glyph stays faint on every column that merely COULD be sorted: it announces that\n the heading can be clicked without competing for attention with the one column\n actually carrying the order. */\n .v-data-table-sort-icon {\n opacity: 0.35;\n }\n\n .v-data-table-sort[data-direction] .v-data-table-sort-icon {\n opacity: 1;\n }\n\n .v-data-table-state {\n padding: var(--vectis-space-6);\n text-align: center;\n color: var(--vectis-color-text-muted);\n }\n\n .v-data-table-state-loading {\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n .v-data-table-footer {\n flex: none;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n gap: var(--vectis-space-4);\n padding-block-start: var(--vectis-space-3);\n padding-block-end: var(--data-table-frame-pad);\n padding-inline: var(--data-table-frame-pad);\n }\n\n /*\n * The right-hand zone is pushed to the edge by an automatic margin rather than by an\n * alignment on the footer. The two zones are siblings in the same row, and the count on the\n * left has to stay at the OPPOSITE edge: any alignment set on the footer would move both of\n * them together.\n */\n .v-data-table-footer-end {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n gap: var(--vectis-space-4);\n margin-inline-start: auto;\n }\n\n .v-data-table-selection,\n .v-data-table-range,\n .v-data-table-per-page-label {\n font-size: var(--vectis-text-body-md-size);\n color: var(--vectis-color-text-muted);\n }\n\n .v-data-table-per-page {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n /*\n * The stacked form: once the COMPONENT is narrow, each row becomes a card carrying its own\n * headings. It is entirely CSS, and the threshold is written as a literal length; these\n * queries accept no variables.\n */\n @container (max-width: 640px) {\n .v-data-table[data-responsive='stack'] .v-data-table-head {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n overflow: hidden;\n clip-path: inset(50%);\n }\n\n /*\n * The heading row is out of sight, so its CONTROLS leave the tab order with it: a sort\n * button or a \"select all\" box the keyboard lands on and nobody can see. `visibility`\n * is what takes a control out of the tab order in CSS, and it takes it out of the\n * accessibility tree too, so a sortable heading hands its name to a plain text copy.\n */\n .v-data-table[data-responsive='stack'] .v-data-table-head :is(.v-data-table-sort, .v-checkbox) {\n visibility: hidden;\n }\n\n .v-data-table[data-responsive='stack'] .v-data-table-stack-label {\n display: inline;\n }\n\n .v-data-table[data-responsive='stack'] tbody tr {\n display: block;\n padding-block: var(--vectis-space-2);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-data-table[data-responsive='stack'] tbody tr:last-child {\n border-block-end: none;\n }\n\n .v-data-table[data-responsive='stack'] td {\n display: flex;\n justify-content: space-between;\n align-items: baseline;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-1) var(--vectis-space-2);\n border: none;\n text-align: end;\n }\n\n /* Each cell writes its own column's name before itself, taken from the attribute the\n template put there. It takes the overline type role, without forcing capitals. */\n .v-data-table[data-responsive='stack'] td::before {\n content: attr(data-label);\n font-size: var(--vectis-text-overline-size);\n font-weight: var(--vectis-text-overline-weight);\n letter-spacing: var(--vectis-text-overline-tracking);\n color: var(--vectis-color-text-muted);\n }\n\n /*\n * The checkbox cell carries no column name; there is none; so it gets no heading and opens\n * the card on its own line.\n */\n .v-data-table[data-responsive='stack'] td.v-data-table-select {\n justify-content: flex-start;\n }\n\n .v-data-table[data-responsive='stack'] td.v-data-table-select::before {\n content: none;\n }\n\n /* In a narrow component the search field drops under the title and takes the whole\n width. This applies to both responsive forms, the toolbar never being part of what\n scrolls. */\n .v-data-table-search {\n flex: 1 1 100%;\n }\n }\n}\n</style>\n"],"mappings":""}
1
+ {"version":3,"file":"VDataTable.js","names":[],"sources":["../../../src/components/VDataTable/VDataTable.vue"],"sourcesContent":["<script setup lang=\"ts\" generic=\"Row extends Record<string, unknown>\">\n// @core\n/**\n * Native table semantics remain intact. JavaScript adds sorting, paging, selection and\n * debounced search; browser measurements stay in browser tests.\n */\n\nimport { computed, ref, useId, useSlots, watch, watchEffect } from 'vue'\nimport type { StyleValue } from 'vue'\n\nimport VButton from '../VButton/VButton.vue'\nimport VCheckbox from '../VCheckbox/VCheckbox.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { iconProps } from '../VIcon/iconProps'\nimport { arrow_downward as arrowDownwardIcon } from '../VIcon/icons/arrow_downward'\nimport { arrow_drop_down as arrowDropDownIcon } from '../VIcon/icons/arrow_drop_down'\nimport { arrow_upward as arrowUpwardIcon } from '../VIcon/icons/arrow_upward'\nimport { search as searchIcon } from '../VIcon/icons/search'\nimport { swap_vert as swapVertIcon } from '../VIcon/icons/swap_vert'\nimport type { IconSource } from '../VIcon/types'\nimport VInput from '../VInput/VInput.vue'\nimport VMenu from '../VMenu/VMenu.vue'\nimport VMenuItem from '../VMenu/VMenuItem.vue'\nimport VPagination from '../VPagination/VPagination.vue'\nimport VSpinner from '../VSpinner/VSpinner.vue'\nimport VTypography from '../VTypography/VTypography.vue'\n\nimport { toggleValue } from '../../utils/array'\nimport { cssSize } from '../../utils/css'\nimport { isDev } from '../../utils/env'\nimport { clamp } from '../../utils/number'\nimport { createNormalizedCache, normalizeText } from '../../utils/text'\n\nimport { useRootAttrs } from '../../composables/useRootAttrs'\n\nimport { useTimer } from '../../composables/useTimer'\nimport { useLocale, useMessages } from '../../i18n/state'\n\n/** How a column's content is aligned. */\nexport type DataTableColumnAlign = 'start' | 'center' | 'end'\n\n/** One column of the table. */\nexport interface DataTableColumn {\n /** Which field of a row it shows, and the name its slots are addressed by. */\n key: string\n /** Its heading. */\n label: string\n /** Lets the reader sort by this column. */\n sortable?: boolean\n /** How its content is aligned. Numbers usually belong at the end. */\n align?: DataTableColumnAlign\n}\n\n/** Which way a column is sorted: A to Z, or Z to A. */\nexport type DataTableSortDirection = 'asc' | 'desc'\n\n/** Which column the table is sorted by, and in which direction. */\nexport interface DataTableSort {\n /** The `key` of the column the rows are ordered by. */\n key: string\n /** Which way they are ordered. */\n direction: DataTableSortDirection\n}\n\n/** How a row is identified: by the field named in `rowKey`, or failing that by its position. */\nexport type DataTableRowId = string | number\n\n/**\n * Everything the table is currently being asked for, reported whenever it changes so a\n * server can answer it. What is not set is `null` rather than left out, so the object keeps\n * the same keys once serialized into a request.\n */\nexport interface DataTableParams {\n /** The page asked for, from 1. */\n page: number\n /** How many rows a page holds, `null` when the table is not paginated. */\n perPage: number | null\n /** The `key` of the sorted column, `null` when the rows are not sorted. */\n sortKey: string | null\n /** Which way they are sorted, `null` when they are not. */\n sortDirection: DataTableSortDirection | null\n /** The committed search, empty when nothing is searched for. */\n search: string\n}\n\n/** What the `cell-<key>` slots receive. */\nexport interface DataTableCellSlotProps<Row extends Record<string, unknown>> {\n /** The row the cell belongs to. */\n row: Row\n /** The value of the column's field in that row. */\n value: unknown\n /** The column the cell belongs to. */\n column: DataTableColumn\n}\n\n/** What the `head-<key>` slots receive. */\nexport interface DataTableHeadSlotProps {\n /** The column the heading belongs to. */\n column: DataTableColumn\n}\n\n/** What the `#empty` slot receives. */\nexport interface DataTableEmptySlotProps {\n /** The search that produced the empty result, empty when nothing was searched for. */\n search: string\n}\n\n/** The rows on show, as `rangeText` receives them, counted from 1. */\nexport interface DataTableRange {\n /** The position of the first row on show. */\n start: number\n /** The position of the last row on show. */\n end: number\n /** How many rows there are in all. */\n total: number\n}\n\n/** How much decoration the table carries. */\nexport type DataTableVariant = 'flat' | 'outlined'\n\n// This interface is exported rather than kept local, and it is generic rather than referring to\n// the component's own type parameter. A component typed over its rows inlines the whole\n// signature of its props into the declarations it emits, so a name that is not exported cannot\n// be written there and the build of the type declarations fails.\nexport interface DataTableProps<Row extends Record<string, unknown>> {\n /** The columns to show, in order. */\n columns: DataTableColumn[]\n /** The rows to show. */\n rows: Row[]\n /** Which field identifies a row. */\n rowKey?: string\n /**\n * A sentence describing what the table holds. It is announced before the table itself,\n * and is what tells a screen reader user whether it is worth exploring.\n */\n caption?: string\n /**\n * How the table is framed: nothing at all, or a card with a raised background, a border\n * and rounded corners.\n */\n variant?: DataTableVariant\n /** Shows that the rows are being loaded. */\n loading?: boolean\n /**\n * What is written beside the spinner while the rows are loading. It falls back to the design\n * system dictionary.\n */\n loadingText?: string\n /** What is said when there is no row to show. It falls back to the design system dictionary. */\n emptyText?: string\n /** A title above the table, on the left of its toolbar. */\n title?: string\n /** Adds a search field to the toolbar. */\n searchable?: boolean\n /** What that field says while empty. It falls back to the design system dictionary. */\n searchPlaceholder?: string\n /**\n * What screen readers announce for the search field. It falls back to the design\n * system dictionary.\n */\n searchLabel?: string\n /**\n * When a server does the searching, how long to wait after a keystroke before asking\n * it, in milliseconds. Zero asks at once.\n */\n searchDebounce?: number\n /** Tints every other row, which helps the eye follow a long line across the table. */\n striped?: boolean\n /**\n * Keeps the column headings in place while the rows scroll under them. It needs a\n * bounded scrolling area to work: either the `height` prop, or a parent with a height\n * of its own.\n */\n stickyHeader?: boolean\n /** Tightens the cells by one step, and everything the table renders with them. */\n compact?: boolean\n /**\n * The height of the whole component, toolbar and pagination included: a number is read as\n * pixels, anything else as a CSS length. Left out, the table takes its parent's height\n * whenever the parent has one.\n */\n height?: number | string\n /** The heading icon of a column that can be sorted but currently is not. */\n sortIcon?: IconSource\n /**\n * The icon of an ascending sort. It points DOWN by default, the spreadsheet\n * convention: sorting A to Z reads downwards.\n */\n sortAscIcon?: IconSource\n /** The icon of a descending sort. */\n sortDescIcon?: IconSource\n /** The choices offered for how many rows a page holds. */\n perPageOptions?: number[]\n /** What that choice is called. It falls back to the design system dictionary. */\n perPageText?: string\n /**\n * How many rows there are in all on the server. It is what lets the pagination and the\n * range be right when the table only ever holds one page.\n */\n total?: number\n /** Shows which rows are being looked at (\"1–10 of 42\") in the footer. */\n showRange?: boolean\n /** Rephrases that range. It falls back to the design system dictionary. */\n rangeText?: (range: DataTableRange) => string\n /** Adds a checkbox to every row, and one in the heading to take the whole page. */\n selectable?: boolean\n /**\n * What the heading checkbox is announced as. It falls back to the design system\n * dictionary.\n */\n selectAllLabel?: string\n /**\n * How the selection is summed up in the footer. It says nothing at all when nothing is\n * selected, and falls back to the design system dictionary.\n */\n selectionText?: (count: number) => string\n /**\n * What a row's checkbox is announced as. \"Select row\" tells a screen reader user nothing\n * about WHICH row, so this is worth supplying with something from the row itself. It falls\n * back to the design system dictionary, which numbers the rows from 1.\n */\n selectRowLabel?: (row: Row, index: number) => string\n /**\n * Hands the searching, the sorting and the paging over to a server: the rows are shown\n * exactly as they arrive, and every change of what is being asked for is reported so\n * the server can answer it.\n */\n serverSide?: boolean\n}\n\nconst props = withDefaults(defineProps<DataTableProps<Row>>(), {\n rowKey: undefined,\n caption: undefined,\n variant: 'flat',\n loading: false,\n loadingText: undefined,\n emptyText: undefined,\n title: undefined,\n searchable: false,\n searchPlaceholder: undefined,\n searchLabel: undefined,\n searchDebounce: 250,\n striped: false,\n stickyHeader: false,\n compact: false,\n height: undefined,\n sortIcon: () => swapVertIcon,\n sortAscIcon: () => arrowDownwardIcon,\n sortDescIcon: () => arrowUpwardIcon,\n perPageOptions: undefined,\n perPageText: undefined,\n total: undefined,\n showRange: false,\n rangeText: undefined,\n selectable: false,\n selectAllLabel: undefined,\n selectionText: undefined,\n selectRowLabel: undefined,\n serverSide: false,\n})\n\nconst m = useMessages()\nconst vectisLocale = useLocale()\nconst resolvedEmptyText = computed(() => props.emptyText ?? m.value.dataTable.empty)\nconst resolvedLoadingText = computed(() => props.loadingText ?? m.value.dataTable.loading)\nconst resolvedSearchPlaceholder = computed(\n () => props.searchPlaceholder ?? m.value.dataTable.searchPlaceholder,\n)\nconst resolvedSearchLabel = computed(() => props.searchLabel ?? m.value.dataTable.searchLabel)\nconst resolvedPerPageText = computed(() => props.perPageText ?? m.value.dataTable.perPage)\nconst resolvedSelectAllLabel = computed(() => props.selectAllLabel ?? m.value.dataTable.selectAll)\n\n/** Which column the rows are sorted by, and in which direction. */\nconst sort = defineModel<DataTableSort | null>('sort', { default: null })\n/** The page being shown, counted from 1. */\nconst page = defineModel<number>('page', { default: 1 })\n/**\n * How many rows a page holds. Any value above zero turns the pagination on, so passing\n * one down without binding it is enough to enable it.\n */\nconst perPage = defineModel<number | undefined>('perPage', { default: undefined })\n/*\n * `sort` is `null` because the TABLE writes that state itself, a third click on a heading\n * clearing the order, and a model the component writes needs a value a consumer can store and\n * compare. `perPage` is `undefined` because nothing here ever clears it: it is simply a model\n * that was not given.\n */\n/** The selected rows, as the identities `rowKey` gives them, never the row objects themselves. */\nconst selected = defineModel<DataTableRowId[]>('selected', { default: () => [] })\n/**\n * What is typed in the search field, empty to begin with. Only the declared columns are\n * searched, accent- and case-insensitively; in `serverSide` mode nothing is filtered here\n * and the term is reported through `update:params` instead.\n */\nconst search = defineModel<string>('search', { default: '' })\n\nconst emit = defineEmits<{\n /**\n * What the table is now being asked for, when a server is answering: the page, the page size,\n * the sort or the search has changed, the last one after its delay.\n */\n 'update:params': [params: DataTableParams]\n}>()\n\ndefineSlots<{\n /** What a cell of a given column shows: a slot named after that column's key. */\n [name: `cell-${string}`]: (scope: DataTableCellSlotProps<Row>) => unknown\n /** What a column's heading shows: a slot named after that column's key. */\n [name: `head-${string}`]: (scope: DataTableHeadSlotProps) => unknown\n /** The left side of the toolbar, replacing the `title` prop. */\n title?(): unknown\n /** What the table shows while its rows are loading, replacing the spinner and its text. */\n loading?(): unknown\n /**\n * What the table shows when there is no row to show, replacing `emptyText`. It receives the\n * search that produced the empty result, empty when nothing was searched for.\n */\n empty?(scope: DataTableEmptySlotProps): unknown\n}>()\n\n// `class` and `style` stay on the wrapper, where a consumer expects to place the component;\n// everything else; an id, the aria-*; goes to the `<table>` itself, the only element they\n// validly describe.\ndefineOptions({ inheritAttrs: false })\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\nconst slots = useSlots()\n\n// A template ref on the component reaches the wrapper, a layout box; the `<table>` is where\n// the consumer's attributes land and what they describe.\nconst tableEl = ref<HTMLTableElement | null>(null)\ndefineExpose({\n /** The `<table>` element, where the consumer's attributes land. */\n el: tableEl,\n})\n\n// @a11y\n/*\n * A caption already does, and so does a consumer's own `aria-label` or `aria-labelledby`, which\n * a reference to the title would override. Bound before the forwarded attributes, like every\n * state the component owns.\n */\nconst titleId = useId()\nconst tableLabelledBy = computed(() =>\n props.caption ||\n !(props.title || slots.title) ||\n attrs['aria-label'] !== undefined ||\n attrs['aria-labelledby'] !== undefined\n ? undefined\n : titleId,\n)\n\n// @devwarn\nif (isDev) {\n if (props.selectable && !props.rowKey)\n console.warn(\n '[VDataTable] `selectable` without `rowKey` — index-based identities are corrupted by sorting, filtering and pagination.',\n )\n /*\n * A key that is missing, or not a string or a number, turns into the same text on every\n * row (\"undefined\"), and two rows sharing a key share an identity: ticking one ticks them\n * all, and the rows' own `:key`s collide. Once per instance and per kind.\n */\n const warned = new Set<string>()\n watchEffect(() => {\n const key = props.rowKey\n if (!key) return\n const seen = new Set<unknown>()\n for (const row of props.rows) {\n const value = row[key]\n if (typeof value !== 'string' && typeof value !== 'number') {\n if (!warned.has('type')) {\n warned.add('type')\n console.warn(\n `[VDataTable] \\`rowKey\\` \"${key}\" is missing or is not a string or a number on some rows: those rows share one identity.`,\n )\n }\n } else if (seen.has(value)) {\n if (!warned.has('duplicate')) {\n warned.add('duplicate')\n console.warn(\n `[VDataTable] \\`rowKey\\` \"${key}\" holds the value \"${value}\" twice: rows are told apart by it, so it must be unique.`,\n )\n }\n } else seen.add(value)\n }\n })\n}\n\n/*\n * A key that is already a string or a number is returned AS IT IS. The selection holds these\n * identities and is looked up with a Set, which tells the number 7 from the string \"7\": turned\n * into text, a table keyed by numeric ids never found a selected row again, so every checkbox\n * stayed unticked.\n */\nfunction rowIdentity(row: Row, index: number): DataTableRowId {\n if (!props.rowKey) return index\n const key = row[props.rowKey]\n return typeof key === 'string' || typeof key === 'number' ? key : String(key)\n}\n\n// Searching fields the reader cannot see would return rows for reasons nothing on screen\n// explains.\n/*\n * The filter below re-reads the whole table on every keystroke: ten thousand rows across five\n * columns is fifty thousand normalizations per character typed without it.\n */\nconst normalizedOf = createNormalizedCache<Row>()\nconst normalizedCell = (row: Row, key: string) => normalizedOf(row, String(row[key] ?? ''), key)\n\nconst filteredRows = computed(() => {\n if (props.serverSide || !props.searchable) return props.rows\n const needle = normalizeText((search.value ?? '').trim())\n if (!needle) return props.rows\n return props.rows.filter((row) =>\n props.columns.some((column) => normalizedCell(row, column.key).includes(needle)),\n )\n})\n\n/*\n * The locale must nonetheless stay EXPLICIT: an `undefined` one resolves differently in Node\n * and in the browser, so the row order would diverge across hydration.\n */\nconst collator = computed(() => new Intl.Collator(vectisLocale.value, { numeric: true }))\n\n/*\n * What a cell is compared by. A `Date` by its instant: as text it collates on `toString()`,\n * which opens with the WEEKDAY, so Monday the 5th sorted before Saturday the 3rd.\n */\nconst NUMERIC_TEXT = /^\\s*[+-]?(\\d+\\.?\\d*|\\.\\d+)(e[+-]?\\d+)?\\s*$/i\nfunction sortKey(value: unknown): unknown {\n if (value instanceof Date) return value.getTime()\n if (typeof value === 'string' && NUMERIC_TEXT.test(value)) return Number(value)\n return value\n}\n\nconst sortedRows = computed(() => {\n const current = sort.value\n if (!current || props.serverSide) return filteredRows.value\n const factor = current.direction === 'asc' ? 1 : -1\n const compare = collator.value.compare\n return [...filteredRows.value].sort((a, b) => {\n const av = sortKey(a[current.key])\n const bv = sortKey(b[current.key])\n if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * factor\n return compare(String(av ?? ''), String(bv ?? '')) * factor\n })\n})\n\nconst paginated = computed(() => (perPage.value ?? 0) > 0)\nconst totalCount = computed(() =>\n props.serverSide ? (props.total ?? props.rows.length) : filteredRows.value.length,\n)\nconst pageCount = computed(() =>\n paginated.value ? Math.max(1, Math.ceil(totalCount.value / (perPage.value ?? 1))) : 1,\n)\n// The page actually shown is CLAMPED by derivation and the model is never rewritten: a\n// search that leaves fewer pages simply falls back to the last one, and typing a\n// character that would have emptied the table does not silently move the consumer's own\n// page number.\nconst currentPage = computed(() => clamp(page.value, 1, pageCount.value))\n\nconst displayedRows = computed(() => {\n // With a server answering, the rows received ARE the current page: cutting them again\n // would show a tenth of a page.\n if (props.serverSide || !paginated.value) return sortedRows.value\n const start = (currentPage.value - 1) * (perPage.value ?? 0)\n return sortedRows.value.slice(start, start + (perPage.value ?? 0))\n})\n\nfunction toggleSort(key: string) {\n const current = sort.value\n if (!current || current.key !== key) sort.value = { key, direction: 'asc' }\n else if (current.direction === 'asc') sort.value = { key, direction: 'desc' }\n else sort.value = null\n}\n\n// @a11y\n// Only the column actually sorted carries the attribute. Setting it on every heading would have a screen reader announce \"not sorted\" on each one in turn, drowning\n// the single piece of information that matters: which column the order comes from.\nfunction ariaSort(column: DataTableColumn): 'ascending' | 'descending' | undefined {\n if (sort.value?.key !== column.key) return undefined\n return sort.value.direction === 'asc' ? 'ascending' : 'descending'\n}\n\n/**\n * Which icon a column's heading shows: a neutral one on any sortable column, and the\n * arrow of the current direction on the one being sorted by.\n *\n * It is deliberately named differently from the prop it falls back to: written the same,\n * the function would shadow the prop inside the template and the icons would never be\n * overridable.\n */\nfunction sortIconFor(column: DataTableColumn): IconSource {\n if (sort.value?.key !== column.key) return props.sortIcon\n return sort.value.direction === 'asc' ? props.sortAscIcon : props.sortDescIcon\n}\n\n// And when a server answers, nothing native waits before asking it, so that has to be written.\n// The waiting is delegated to `useTimer`; a delay of zero running at once, cancellation when\n// the component goes away; and the term that was actually committed is what the parameters\n// below report.\nconst committedSearch = ref(search.value)\nconst searchTimer = useTimer()\n\nfunction commitSearch() {\n // A term typed and erased inside the debounce is no new search: going back to page 1\n // and asking the server again would be a request for what is already on screen.\n if (search.value === committedSearch.value) return\n committedSearch.value = search.value\n page.value = 1\n}\n\nwatch(search, () => {\n if (!props.serverSide) {\n page.value = 1\n return\n }\n searchTimer.start(commitSearch, props.searchDebounce)\n})\n\nconst emptySearch = computed(() => (props.serverSide ? committedSearch.value : search.value))\n\n/*\n * `page` is the model, never the clamped `currentPage`. The clamp reads `total`, which is the\n * server's own answer: read here, a response that changed the total would move the parameters\n * and ask for the page again, and a consumer clearing `total` while it loads would send the\n * reader back to page 1.\n */\n// Committing a search and going back to the first page happen one after the other, but both\n// land in the same flush, so this runs once on the final values; never two requests for what\n// the reader experienced as a single action. Compared by value: a parent handing down an equal\n// `sort` object is no new request.\nwatch(\n [\n page,\n () => perPage.value ?? null,\n () => sort.value?.key ?? null,\n () => sort.value?.direction ?? null,\n committedSearch,\n ],\n ([page, perPage, sortKey, sortDirection, search]) => {\n if (props.serverSide) emit('update:params', { page, perPage, sortKey, sortDirection, search })\n },\n)\n\n// The selection. The heading checkbox covers the rows currently VISIBLE and not the whole\n// table: a box that silently selected forty thousand rows would be a trap.\nconst selectedSet = computed(() => new Set<DataTableRowId>(selected.value))\nconst visibleIds = computed(() => displayedRows.value.map((row, index) => rowIdentity(row, index)))\nconst allVisibleSelected = computed(\n () => visibleIds.value.length > 0 && visibleIds.value.every((id) => selectedSet.value.has(id)),\n)\nconst masterIndeterminate = computed(\n () => !allVisibleSelected.value && visibleIds.value.some((id) => selectedSet.value.has(id)),\n)\n\n/*\n * A row is asked about by the identity `visibleIds` already holds for it: the template reads\n * `visibleIds[index]` for the key, the tint and the checkbox alike, rather than deriving it\n * again three or four times per row per render.\n */\nconst isSelected = (id: DataTableRowId) => selectedSet.value.has(id)\n\nfunction toggleRow(id: DataTableRowId) {\n selected.value = toggleValue(selected.value, id)\n}\n\n// It only ever touches the rows on screen, so what was selected on the other pages\n// survives moving between them.\n// Refused with no row on screen, or while the rows on screen are the PREVIOUS ones behind\n// the loading state: ticked there, it stayed ticked with nothing selected, or selected rows\n// the reader could not see.\nconst masterDisabled = computed(() => visibleIds.value.length === 0 || props.loading)\n\nfunction toggleMaster() {\n if (masterDisabled.value) return\n const ids = visibleIds.value\n if (allVisibleSelected.value) {\n const visible = new Set(ids)\n selected.value = selected.value.filter((id) => !visible.has(id))\n } else {\n const current = selectedSet.value\n selected.value = [...selected.value, ...ids.filter((id) => !current.has(id))]\n }\n}\n\n// @a11y\n// Without this every checkbox in the column would be announced identically, and a screen\n// reader user would have no way of knowing which row they were about to select.\nfunction rowSelectLabel(row: Row, index: number): string {\n // The position in the whole table, or every page would have its own \"Select row 1\". The\n // dictionary counts from one as a human does, the prop from zero as code does.\n const position = (paginated.value ? (currentPage.value - 1) * (perPage.value ?? 0) : 0) + index\n return props.selectRowLabel?.(row, position) ?? m.value.dataTable.selectRow(position + 1)\n}\n\nwatch(perPage, () => {\n page.value = 1\n})\n\nconst colCount = computed(() => props.columns.length + (props.selectable ? 1 : 0))\n\n// @a11y\n/**\n * It is rendered, empty, as soon as rows can be selected at all. A region meant to announce its\n * own changes must already exist before the first change: inserted into the page at the same\n * moment as its text, it announces nothing, and the first row selected would pass in silence.\n */\nconst selectionSummary = computed(() => {\n const count = selected.value.length\n if (count === 0) return ''\n if (props.selectionText) return props.selectionText(count)\n return m.value.dataTable.selection(count)\n})\n\nconst rangeSummary = computed(() => {\n const per = perPage.value ?? 0\n const total = totalCount.value\n const start = total === 0 ? 0 : (currentPage.value - 1) * per + 1\n const end = Math.min(currentPage.value * per, total)\n return props.rangeText?.({ start, end, total }) ?? m.value.dataTable.range({ start, end, total })\n})\n\n// The height is applied to the whole component and not as a ceiling on the scrolling area. A\n// soft ceiling remains available to the consumer through a maximum height of their own: the\n// component is a column whose scrolling area can be compressed.\nconst heightStyle = computed<StyleValue | undefined>(() =>\n props.height !== undefined ? { blockSize: cssSize(props.height) } : undefined,\n)\n</script>\n\n<template>\n <div\n class=\"v-data-table\"\n :class=\"rootClass\"\n :style=\"[heightStyle, rootStyle]\"\n :data-variant=\"variant\"\n :data-striped=\"striped ? '' : undefined\"\n :data-compact=\"compact ? '' : undefined\"\n :data-sticky-header=\"stickyHeader ? '' : undefined\"\n :data-selectable=\"selectable ? '' : undefined\"\n >\n <div v-if=\"title || $slots.title || searchable\" class=\"v-data-table-toolbar\">\n <VTypography :id=\"titleId\" as=\"div\" variant=\"heading-4\" class=\"v-data-table-title\">\n <slot name=\"title\">{{ title }}</slot>\n </VTypography>\n <VInput\n v-if=\"searchable\"\n v-model=\"search\"\n class=\"v-data-table-search\"\n type=\"search\"\n size=\"sm\"\n :compact=\"compact\"\n :icon-start=\"searchIcon\"\n clearable\n :placeholder=\"resolvedSearchPlaceholder\"\n :aria-label=\"resolvedSearchLabel\"\n />\n </div>\n\n <div class=\"v-data-table-scroller\">\n <table\n ref=\"tableEl\"\n class=\"v-data-table-table\"\n :aria-labelledby=\"tableLabelledBy\"\n v-bind=\"forwardedAttrs\"\n >\n <caption v-if=\"caption\" class=\"v-data-table-caption\">\n {{\n caption\n }}\n </caption>\n <thead class=\"v-data-table-head\">\n <tr>\n <th v-if=\"selectable\" scope=\"col\" class=\"v-data-table-select\">\n <VCheckbox\n :model-value=\"allVisibleSelected\"\n :indeterminate=\"masterIndeterminate\"\n :disabled=\"masterDisabled\"\n :aria-label=\"resolvedSelectAllLabel\"\n @update:model-value=\"toggleMaster\"\n />\n </th>\n <th\n v-for=\"column in columns\"\n :key=\"column.key\"\n scope=\"col\"\n :data-align=\"column.align\"\n :aria-sort=\"ariaSort(column)\"\n >\n <button\n v-if=\"column.sortable\"\n type=\"button\"\n class=\"v-data-table-sort\"\n :data-direction=\"sort?.key === column.key ? sort.direction : undefined\"\n @click=\"toggleSort(column.key)\"\n >\n <slot :name=\"`head-${column.key}`\" :column=\"column\">{{ column.label }}</slot>\n <!-- Decorative, and deliberately so: given no label, an icon hides itself\n from screen readers. What the sort state is, is already carried by\n the heading itself. -->\n <VIcon class=\"v-data-table-sort-icon\" v-bind=\"iconProps(sortIconFor(column))\" />\n </button>\n <template v-else>\n <slot :name=\"`head-${column.key}`\" :column=\"column\">{{ column.label }}</slot>\n </template>\n </th>\n </tr>\n </thead>\n <tbody>\n <!--\n The order matters: loading is checked before emptiness, so a table waiting for its\n rows never claims there are none.\n -->\n <tr v-if=\"loading\">\n <td :colspan=\"colCount\" class=\"v-data-table-state\">\n <slot name=\"loading\">\n <!-- The spinner carries the text for a screen reader, so the visible copy is\n hidden from it: read twice, the cell would say it is loading twice. -->\n <span class=\"v-data-table-state-loading\">\n <VSpinner :label=\"resolvedLoadingText\" />\n <span aria-hidden=\"true\">{{ resolvedLoadingText }}</span>\n </span>\n </slot>\n </td>\n </tr>\n <tr v-else-if=\"displayedRows.length === 0\">\n <td :colspan=\"colCount\" class=\"v-data-table-state\">\n <slot name=\"empty\" :search=\"emptySearch\">{{ resolvedEmptyText }}</slot>\n </td>\n </tr>\n <template v-else>\n <tr\n v-for=\"(row, index) in displayedRows\"\n :key=\"visibleIds[index]\"\n :data-selected=\"selectable && isSelected(visibleIds[index]!) ? '' : undefined\"\n >\n <!--\n The selection is marked with a plain attribute and not with the ARIA selected\n state, which is invalid on the row of a table: it belongs to a grid. What tells\n assistive technology that a row is selected is its checkbox being checked.\n -->\n <td v-if=\"selectable\" class=\"v-data-table-select\">\n <VCheckbox\n :model-value=\"isSelected(visibleIds[index]!)\"\n :aria-label=\"rowSelectLabel(row, index)\"\n @update:model-value=\"toggleRow(visibleIds[index]!)\"\n />\n </td>\n <td v-for=\"column in columns\" :key=\"column.key\" :data-align=\"column.align\">\n <slot\n :name=\"`cell-${column.key}`\"\n :row=\"row\"\n :value=\"row[column.key]\"\n :column=\"column\"\n >\n {{ row[column.key] }}\n </slot>\n </td>\n </tr>\n </template>\n </tbody>\n </table>\n </div>\n\n <!-- The footer has two zones: what is selected on the left, and on the right the page\n size, the range and the pagination, in that order. -->\n <div v-if=\"paginated || selectable\" class=\"v-data-table-footer\">\n <span v-if=\"selectable\" class=\"v-data-table-selection\" aria-live=\"polite\">{{\n selectionSummary\n }}</span>\n <div v-if=\"paginated\" class=\"v-data-table-footer-end\">\n <div v-if=\"perPageOptions?.length\" class=\"v-data-table-per-page\">\n <span class=\"v-data-table-per-page-label\" aria-hidden=\"true\">{{\n resolvedPerPageText\n }}</span>\n <!-- The panel is told to match its trigger, which here replaces the default\n minimum width with something sensible: a menu of \"10\", \"25\", \"50\" has no\n use for the width a menu of commands assumes, and it still cannot end up\n narrower than the button opening it. -->\n <VMenu size=\"sm\" :compact=\"compact\" placement=\"top-end\" match-trigger>\n <template #trigger=\"{ triggerProps }\">\n <VButton\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n :compact=\"compact\"\n v-bind=\"triggerProps\"\n :aria-label=\"m.dataTable.perPageValue(resolvedPerPageText, perPage ?? 0)\"\n >\n {{ perPage }}\n <VIcon :name=\"arrowDropDownIcon\" />\n </VButton>\n </template>\n <VMenuItem\n v-for=\"option in perPageOptions\"\n :key=\"option\"\n :label=\"String(option)\"\n :selected=\"option === perPage\"\n @select=\"perPage = option\"\n />\n </VMenu>\n </div>\n <span v-if=\"showRange\" class=\"v-data-table-range\" aria-live=\"polite\">{{\n rangeSummary\n }}</span>\n <!--\n Named after the table rather than with the generic pagination wording: a page holding\n this table and a pagination of its own would otherwise expose two navigation landmarks\n with the same name, and a screen reader user could not tell them apart.\n -->\n <VPagination\n v-model=\"page\"\n :label=\"m.dataTable.pagination\"\n :length=\"pageCount\"\n size=\"sm\"\n :compact=\"compact\"\n align=\"end\"\n :total-visible=\"0\"\n edge-controls\n detached\n />\n </div>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-data-table {\n /*\n * It does not go through the shared control scale; there is no single control height in a\n * table; which is the same case VAccordion is in.\n */\n --data-table-pad-block: var(--vectis-space-3);\n --data-table-pad-inline: var(--vectis-space-3);\n --data-table-head-pad-block: var(--vectis-space-2);\n\n /* The gutter between the frame and what it holds: nothing when the table is\n unframed, so the caption, the toolbar and the footer sit flush with the edge, and\n the cells' own inline padding as soon as a frame appears. */\n --data-table-frame-pad: 0px;\n\n font-family: var(--vectis-text-family);\n\n /*\n * Deliberately no `min-block-size: 0` here. Inside a flex column parent too short for it,\n * the automatic minimum is what keeps the table at its natural height instead of letting it\n * be crushed to nothing.\n */\n display: flex;\n flex-direction: column;\n block-size: 100%;\n }\n\n .v-data-table[data-compact] {\n --data-table-pad-block: var(--vectis-space-2);\n --data-table-pad-inline: var(--vectis-space-2);\n --data-table-head-pad-block: var(--vectis-space-1);\n }\n\n /*\n * The card. The unframed default has nothing to undo, since it declares no decoration at all;\n * whatever surrounds the table is what provides the surface then.\n */\n .v-data-table[data-variant='outlined'] {\n --data-table-frame-pad: var(--data-table-pad-inline);\n\n background: var(--vectis-color-surface-raised);\n border: 1px solid var(--vectis-color-border);\n border-radius: var(--vectis-radius-surface);\n /*\n * `clip` and not `hidden`. Hiding the overflow would make this element a scroll container\n * of its own, and the sticky heading would then anchor to IT rather than to the area that\n * actually scrolls; which is to say it would not stick at all.\n */\n overflow: clip;\n }\n\n /*\n * `min-block-size: 0` is load-bearing; a flex item refuses by default to shrink below its\n * content, so without it the area never compresses and the table overflows instead of\n * scrolling.\n */\n .v-data-table-scroller {\n flex: 1 1 auto;\n min-block-size: 0;\n overflow: auto;\n }\n\n .v-data-table-toolbar {\n flex: none;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--vectis-space-3);\n padding-block-start: var(--data-table-frame-pad);\n padding-block-end: var(--vectis-space-3);\n padding-inline: var(--data-table-frame-pad);\n }\n\n /*\n * It is set through the colour VARIABLE that component reads, and not with a plain colour\n * declaration. That declaration would collide with VTypography's own at equal specificity,\n * and the winner would be decided by whichever sheet the consumer's bundler put last.\n */\n .v-data-table-title {\n --typography-color: var(--vectis-color-text);\n }\n\n /* The search field is given a width of its own, overriding the full width VInput takes\n by default. The selector is qualified by its context, which makes it one step more\n specific than VInput's own rule and therefore independent of the order the two sheets\n end up in. */\n .v-data-table-toolbar .v-input {\n inline-size: var(--vectis-control-size-table-search);\n max-inline-size: 100%;\n }\n\n .v-data-table-table {\n width: 100%;\n border-collapse: collapse;\n font-size: var(--vectis-text-body-md-size);\n color: var(--vectis-color-text);\n }\n\n .v-data-table-caption {\n padding-block-end: var(--vectis-space-3);\n padding-inline: var(--data-table-frame-pad);\n text-align: start;\n font-size: var(--vectis-text-body-md-size);\n color: var(--vectis-color-text-muted);\n }\n\n .v-data-table-table th {\n padding: var(--data-table-head-pad-block) var(--data-table-pad-inline);\n text-align: start;\n font-size: var(--vectis-text-body-md-size);\n /*\n * The heavier weight distinguishes a heading from the data under it, which is emphasis\n * rather than a typographic role; hence a font token read directly.\n */\n font-weight: var(--vectis-font-weight-semibold);\n color: var(--vectis-color-text-muted);\n /*\n * The heading row gets a tint of its own so it reads apart from the data. `muted` and not\n * `sunken`: the striped rows already take `sunken`, and a heading painted the same would\n * read as one more stripe.\n */\n background-color: var(--vectis-color-surface-muted);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-data-table-table td {\n padding: var(--data-table-pad-block) var(--data-table-pad-inline);\n border-block-end: 1px solid var(--vectis-color-border);\n }\n\n .v-data-table-table tbody tr:last-child td {\n border-block-end: none;\n }\n\n .v-data-table-table [data-align='end'] {\n text-align: end;\n }\n\n .v-data-table-table [data-align='center'] {\n text-align: center;\n }\n\n /* The checkbox column is reduced to the width of its content. A table lays its columns\n out automatically, so asking for no width at all is what makes it take the least\n possible. */\n .v-data-table-table .v-data-table-select {\n inline-size: 0;\n }\n\n .v-data-table[data-striped] tbody tr:nth-child(even) {\n background-color: var(--vectis-color-surface-sunken);\n }\n\n /* Placed after the striping on purpose: the specificity is the same, so it is the order\n that makes a selected row keep its tint on both odd and even rows. */\n .v-data-table[data-selectable] tbody tr[data-selected] {\n background-color: var(--vectis-color-accent-surface);\n }\n\n /* Forced colours flatten both tints to `Canvas`: the stripes are only a reading aid, but\n a selected row would be told apart by its checkbox alone. It draws an inward outline in\n the system selection colour, which the mode keeps. */\n @media (forced-colors: active) {\n .v-data-table[data-selectable] tbody tr[data-selected] {\n outline: var(--vectis-focus-ring-width) solid Highlight;\n outline-offset: calc(-1 * var(--vectis-focus-ring-width));\n }\n }\n\n /*\n * A frozen heading relies on the opaque background every `th` already carries: the rows\n * scroll underneath it, and a consumer restyling that background with a translucent colour\n * will see them through.\n */\n .v-data-table[data-sticky-header] th {\n position: sticky;\n inset-block-start: 0;\n z-index: 1;\n }\n\n .v-data-table-sort {\n /*\n * The icon context for the sort glyph. Without it the icon would fall back to one em; the\n * heading's own text size; and come out visibly smaller than every other icon in the\n * component.\n */\n --vectis-icon-size: var(--vectis-icon-size-md);\n --vectis-icon-opsz: 20;\n\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-1);\n border: none;\n background: transparent;\n padding: 0;\n color: inherit;\n font: inherit;\n cursor: pointer;\n /* Seen only on the focus ring: the control radius capped at half a line, VBreadcrumb's\n link recipe, since a heading that wraps would otherwise round to half its height. */\n border-radius: min(var(--vectis-radius-interactive), 0.5lh);\n }\n\n .v-data-table-sort:hover {\n color: var(--vectis-color-text);\n }\n\n .v-data-table-sort:focus-visible {\n outline: var(--vectis-focus-ring-width) solid var(--vectis-focus-ring-color);\n outline-offset: var(--vectis-focus-ring-offset);\n }\n\n /* The glyph stays faint on every column that merely COULD be sorted: it announces that\n the heading can be clicked without competing for attention with the one column\n actually carrying the order. */\n .v-data-table-sort-icon {\n opacity: 0.35;\n }\n\n .v-data-table-sort[data-direction] .v-data-table-sort-icon {\n opacity: 1;\n }\n\n .v-data-table-state {\n padding: var(--vectis-space-6);\n text-align: center;\n color: var(--vectis-color-text-muted);\n }\n\n .v-data-table-state-loading {\n display: inline-flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n\n .v-data-table-footer {\n flex: none;\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n gap: var(--vectis-space-4);\n padding-block-start: var(--vectis-space-3);\n padding-block-end: var(--data-table-frame-pad);\n padding-inline: var(--data-table-frame-pad);\n }\n\n /*\n * The right-hand zone is pushed to the edge by an automatic margin rather than by an\n * alignment on the footer. The two zones are siblings in the same row, and the count on the\n * left has to stay at the OPPOSITE edge: any alignment set on the footer would move both of\n * them together.\n */\n .v-data-table-footer-end {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: flex-end;\n gap: var(--vectis-space-4);\n margin-inline-start: auto;\n }\n\n .v-data-table-selection,\n .v-data-table-range,\n .v-data-table-per-page-label {\n font-size: var(--vectis-text-body-md-size);\n color: var(--vectis-color-text-muted);\n }\n\n .v-data-table-per-page {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-2);\n }\n}\n</style>\n"],"mappings":""}