vectis-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/VColorPicker/color.js +2 -2
- package/dist/components/VColorPicker/color.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.d.ts +10 -0
- package/dist/components/VCombobox/VCombobox.js.map +1 -1
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js +177 -159
- package/dist/components/VCombobox/VCombobox.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.css +1 -1
- package/dist/components/VDataTable/VDataTable.js.map +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/components/VDataTable/VDataTable.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.css +1 -1
- package/dist/components/VDatePicker/VDatePicker.js.map +1 -1
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/components/VDatePicker/VDatePicker.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDialog/VDialog.css +1 -1
- package/dist/components/VDialog/VDialog.js.map +1 -1
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/VDialog/VDialog.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VDrawer/VDrawer.css +1 -1
- package/dist/components/VDrawer/VDrawer.js.map +1 -1
- package/dist/components/VDrawer/VDrawer.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/components/VDrawer/VDrawer.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMeter/VMeter.css +1 -1
- package/dist/components/VMeter/VMeter.d.ts +24 -8
- package/dist/components/VMeter/VMeter.js.map +1 -1
- package/dist/components/VMeter/VMeter.vue_vue_type_script_setup_true_lang.js +65 -48
- package/dist/components/VMeter/VMeter.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VMeterGroup/VMeterGroup.css +2 -0
- package/dist/components/VMeterGroup/VMeterGroup.d.ts +101 -0
- package/dist/components/VMeterGroup/VMeterGroup.js +9 -0
- package/dist/components/VMeterGroup/VMeterGroup.js.map +1 -0
- package/dist/components/VMeterGroup/VMeterGroup.vue_vue_type_script_setup_true_lang.js +117 -0
- package/dist/components/VMeterGroup/VMeterGroup.vue_vue_type_script_setup_true_lang.js.map +1 -0
- package/dist/components/VNumberInput/number.js +2 -2
- package/dist/components/VNumberInput/number.js.map +1 -1
- package/dist/components/VProgressCircular/VProgressCircular.d.ts +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.css +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.d.ts +12 -13
- package/dist/components/VProgressLinear/VProgressLinear.js.map +1 -1
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js +65 -48
- package/dist/components/VProgressLinear/VProgressLinear.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.css +1 -1
- package/dist/components/VSeparator/VSeparator.d.ts +42 -2
- package/dist/components/VSeparator/VSeparator.js.map +1 -1
- package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js +50 -9
- package/dist/components/VSeparator/VSeparator.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.d.ts +6 -0
- package/dist/components/VSideNavigation/VSideNavigation.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js +11 -2
- package/dist/components/VSideNavigation/VSideNavigation.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.css +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.js.map +1 -1
- package/dist/components/VSideNavigation/VSideNavigationItem.vue_vue_type_script_setup_true_lang.js.map +1 -1
- package/dist/components/VSnackbar/state.js +2 -3
- package/dist/components/VSnackbar/state.js.map +1 -1
- package/dist/components/VToast/state.js +2 -3
- package/dist/components/VToast/state.js.map +1 -1
- package/dist/composables/useAriaLabel.d.ts +5 -0
- package/dist/composables/useAriaLabel.js +5 -1
- package/dist/composables/useAriaLabel.js.map +1 -1
- package/dist/composables/useListbox.d.ts +2 -0
- package/dist/composables/useListbox.js +71 -68
- package/dist/composables/useListbox.js.map +1 -1
- package/dist/i18n/en.js +3 -1
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/fr.js +3 -1
- package/dist/i18n/fr.js.map +1 -1
- package/dist/i18n/types.d.ts +6 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +21 -20
- package/dist/styles.css +1 -1
- package/dist/tokens/index.d.ts +10 -5
- package/dist/tokens/semantic.d.ts +10 -5
- package/dist/tokens/semantic.js +10 -5
- package/dist/tokens/semantic.js.map +1 -1
- package/dist/tokens.json +39 -14
- package/package.json +3 -3
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-dialog{--dialog-width:var(--vectis-control-size-dialog-width);inline-size:var(--dialog-width);max-inline-size:calc(100dvi - 2 * var(--vectis-space-4));max-block-size:calc(100dvb - 2 * var(--vectis-space-4));border-radius:var(--vectis-radius-overlay);background:var(--vectis-color-surface-overlay);color:var(--vectis-color-text);box-shadow:var(--vectis-shadow-xl);font-family:var(--vectis-text-family);border:none;flex-direction:column;margin:auto;padding:0;display:flex;overflow:hidden}.v-dialog:not([open]){display:none}.v-dialog:focus-visible{outline:none}.v-dialog-scroll{overscroll-behavior:contain;flex-direction:column;flex:auto;min-block-size:0;display:flex;overflow-y:auto;container-type:scroll-state}.v-dialog-edge{background:0 0;flex:none;block-size:1px;position:sticky}.v-dialog-edge
|
|
2
|
+
@layer vectis.components{.v-dialog{--dialog-width:var(--vectis-control-size-dialog-width);inline-size:var(--dialog-width);max-inline-size:calc(100dvi - 2 * var(--vectis-space-4));max-block-size:calc(100dvb - 2 * var(--vectis-space-4));border-radius:var(--vectis-radius-overlay);background:var(--vectis-color-surface-overlay);color:var(--vectis-color-text);box-shadow:var(--vectis-shadow-xl);font-family:var(--vectis-text-family);border:none;flex-direction:column;margin:auto;padding:0;display:flex;overflow:hidden}.v-dialog:not([open]){display:none}.v-dialog:focus-visible{outline:none}.v-dialog-scroll{overscroll-behavior:contain;flex-direction:column;flex:auto;min-block-size:0;display:flex;overflow-y:auto;container-type:scroll-state}.v-dialog-edge{background:0 0;flex:none;block-size:1px;position:sticky}.v-dialog-edge[data-edge=top]{margin-block-end:-1px;inset-block-start:0}.v-dialog-edge[data-edge=bottom]{margin-block-start:-1px;inset-block-end:0}.v-dialog-header{justify-content:space-between;align-items:flex-start;gap:var(--vectis-space-4);padding:var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);flex:none;display:flex}.v-dialog-titles{flex-direction:column;min-inline-size:0;display:flex}.v-dialog-header-actions{align-items:center;gap:var(--vectis-space-1);margin-block-start:calc(-1 * var(--vectis-space-1));margin-inline-end:calc(-1 * var(--vectis-space-2));display:flex}.v-dialog-body{padding:var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);font-size:var(--vectis-text-body-md-size);line-height:var(--vectis-text-body-md-leading);flex:1 0 auto}.v-dialog-footer{justify-content:flex-end;align-items:center;gap:var(--vectis-space-3);padding:var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);flex-wrap:wrap;flex:none;display:flex}@container scroll-state(scrollable:top){.v-dialog-edge[data-edge=top]{background:var(--vectis-color-border)}}@container scroll-state(scrollable:bottom){.v-dialog-edge[data-edge=bottom]{background:var(--vectis-color-border)}}.v-dialog{transition:opacity var(--vectis-duration-base) var(--vectis-ease-default), transform var(--vectis-duration-base) var(--vectis-ease-default)}@starting-style{.v-dialog[open]{opacity:0;transform:scale(.97)}}.v-dialog::backdrop{background:var(--vectis-color-backdrop);transition:opacity var(--vectis-duration-base) var(--vectis-ease-default)}@starting-style{.v-dialog[open]::backdrop{opacity:0}}@media (prefers-reduced-motion:reduce){.v-dialog,.v-dialog::backdrop{transition:none}}@media (forced-colors:active){.v-dialog{outline:1px solid canvastext}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VDialog.js","names":["_sfc_main","VDialog_vue_vue_type_script_setup_true_lang_default","VDialog_default"],"sources":["../../../src/components/VDialog/VDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/** A native modal `<dialog>`, its model bridged by useModalDialog. */\n\nimport { computed, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** How assistive technology announces the dialog. */\nexport type DialogRole = 'dialog' | 'alertdialog'\n\ninterface DialogProps {\n /**\n * The title of the dialog, which also names it for assistive technology. It is\n * ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, explaining what the dialog is asking. */\n subtitle?: string\n /**\n * How wide the dialog is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-dialog-width` token, 400px by default.\n */\n width?: number | string\n /**\n * What kind of dialog this is. `alertdialog` is for one that must be answered\n * explicitly, and it makes screen readers announce it more insistently. VDialogAlert is\n * exactly that.\n */\n role?: DialogRole\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the\n * backdrop and whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the dialog from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the dialog. Note that refusing Escape while the\n * backdrop still closes cannot be expressed natively, so both routes are then\n * allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n role: 'dialog',\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the dialog is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\n/** What the trigger has to carry: the click that opens the dialog, and the fact that it does. */\nexport type DialogTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n}\n\ndefineSlots<{\n /** The body of the dialog. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross: a menu, a full-screen toggle. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the dialog. */\n footer?(): unknown\n /**\n * The button that opens the dialog. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the dialog itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the dialog, exactly as setting `open` does. The opening lands on the next tick,\n * once the element has been rendered: `el` is still null right after the call, and the\n * promise returned settles once the dialog is showing.\n */\n show,\n /**\n * Closes the dialog. It closes unconditionally, `persistentEscape` and\n * `persistentBackdrop` governing only the two routes the reader can take.\n */\n close,\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-dialog\"\n :style=\"{ '--dialog-width': cssSize(width) }\"\n :role=\"role === 'alertdialog' ? 'alertdialog' : undefined\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n The header and the footer stay put while only the body scrolls, so the scrollbar\n is confined to the body and never runs along them.\n\n The lines that appear under the header and above the footer once the body is\n scrolled are drawn by two sentinels stuck INSIDE the scrolling area, and not by\n the header and footer themselves: a container query can only style what is inside\n the container it asks about, never a sibling of it.\n -->\n <header class=\"v-dialog-header\">\n <slot name=\"header\">\n <div class=\"v-dialog-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-dialog-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-dialog-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-dialog-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-dialog-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-dialog-scroll\">\n <span class=\"v-dialog-edge v-dialog-edge--top\" aria-hidden=\"true\" />\n <div class=\"v-dialog-body\">\n <slot />\n </div>\n <span class=\"v-dialog-edge v-dialog-edge--bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-dialog-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-dialog {\n /*\n * The token is declared on the element rather than written as a `var()` fallback. A custom\n * property inherits, and a dialog opened from inside another one is its DOM descendant:\n * with a fallback, an alert confirming something in an 800px dialog would read that\n * dialog's inline width and open 800px wide itself, with no error anywhere.\n */\n --dialog-width: var(--vectis-control-size-dialog-width);\n inline-size: var(--dialog-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(100dvb - 2 * var(--vectis-space-4));\n /*\n * Restore the modal's automatic margins in the component layer because the reset removes\n * native dialog centring.\n */\n margin: auto;\n /* The header and footer stay put while only the body scrolls, and hiding the\n overflow is what keeps the content inside the rounded corners. */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * An indispensable guard. The browser hides a closed dialog with a rule of its own, but any\n * display we declare above beats it; so without this, a closed dialog would sit in the page\n * at the top left, swallowing clicks.\n */\n .v-dialog:not([open]) {\n display: none;\n }\n\n /* Opening a modal puts the focus on the dialog itself. No ring is drawn around the\n whole thing for that: the focus a reader needs to see is the one on the controls\n inside. */\n .v-dialog:focus-visible {\n outline: none;\n }\n\n .v-dialog-scroll {\n /*\n * The zero minimum is load-bearing: without it a flex item refuses to shrink below its\n * content, and nothing would ever scroll.\n */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n /* A wheel reaching the end of the content stops there rather than scrolling the page\n behind the modal. */\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n /*\n * This makes the box something its descendants can ask questions about; namely whether\n * there is content hidden above or below. It adds no containment of size.\n */\n container-type: scroll-state;\n }\n\n /*\n * The two sentinels: hairlines stuck to the top and the bottom of the scrolling area, and\n * descendants of it, which allows them to ask about its scroll state. Their negative margins\n * mean they occupy no space at all.\n */\n .v-dialog-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-dialog-edge--top {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-dialog-edge--bottom {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-dialog-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-dialog-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n /* The title and the subtitle are rendered by VTypography, which carries their type.\n Their classes remain as hooks, for a consumer's overrides and for the tests. */\n\n .v-dialog-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* The cross is a button, so it carries invisible padding of its own; pulling the\n row back by that amount is what makes the glyph line up with the header's edge\n rather than floating inside it. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-dialog-body {\n /* Short content is stretched to fill the space, so the footer stays at the bottom\n of the dialog rather than floating halfway up; long content keeps its natural\n height, overflows, and is what scrolls. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-dialog-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where it is missing the sentinels simply stay transparent, which is the intended fallback. */\n @container scroll-state(scrollable: top) {\n .v-dialog-edge--top {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-dialog-edge--bottom {\n background: var(--vectis-color-border);\n }\n }\n\n /*\n * The dialog animates on the way in and not on the way out. Since it is removed from the page\n * the moment it closes, there is nothing left to animate out; that is the price of the clean\n * slate the lazy rendering buys, and it also spares the extra declarations an exit animation\n * would need.\n */\n .v-dialog {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-dialog::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-dialog,\n .v-dialog::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-dialog {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VDialog.js","names":["_sfc_main","VDialog_vue_vue_type_script_setup_true_lang_default","VDialog_default"],"sources":["../../../src/components/VDialog/VDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/** A native modal `<dialog>`, its model bridged by useModalDialog. */\n\nimport { computed, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** How assistive technology announces the dialog. */\nexport type DialogRole = 'dialog' | 'alertdialog'\n\ninterface DialogProps {\n /**\n * The title of the dialog, which also names it for assistive technology. It is\n * ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, explaining what the dialog is asking. */\n subtitle?: string\n /**\n * How wide the dialog is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-dialog-width` token, 400px by default.\n */\n width?: number | string\n /**\n * What kind of dialog this is. `alertdialog` is for one that must be answered\n * explicitly, and it makes screen readers announce it more insistently. VDialogAlert is\n * exactly that.\n */\n role?: DialogRole\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the\n * backdrop and whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the dialog from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the dialog. Note that refusing Escape while the\n * backdrop still closes cannot be expressed natively, so both routes are then\n * allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n role: 'dialog',\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the dialog is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\n/** What the trigger has to carry: the click that opens the dialog, and the fact that it does. */\nexport type DialogTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n}\n\ndefineSlots<{\n /** The body of the dialog. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross: a menu, a full-screen toggle. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the dialog. */\n footer?(): unknown\n /**\n * The button that opens the dialog. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the dialog itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the dialog, exactly as setting `open` does. The opening lands on the next tick,\n * once the element has been rendered: `el` is still null right after the call, and the\n * promise returned settles once the dialog is showing.\n */\n show,\n /**\n * Closes the dialog. It closes unconditionally, `persistentEscape` and\n * `persistentBackdrop` governing only the two routes the reader can take.\n */\n close,\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-dialog\"\n :style=\"{ '--dialog-width': cssSize(width) }\"\n :role=\"role === 'alertdialog' ? 'alertdialog' : undefined\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n The header and the footer stay put while only the body scrolls, so the scrollbar\n is confined to the body and never runs along them.\n\n The lines that appear under the header and above the footer once the body is\n scrolled are drawn by two sentinels stuck INSIDE the scrolling area, and not by\n the header and footer themselves: a container query can only style what is inside\n the container it asks about, never a sibling of it.\n -->\n <header class=\"v-dialog-header\">\n <slot name=\"header\">\n <div class=\"v-dialog-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-dialog-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-dialog-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-dialog-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-dialog-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-dialog-scroll\">\n <span class=\"v-dialog-edge\" data-edge=\"top\" aria-hidden=\"true\" />\n <div class=\"v-dialog-body\">\n <slot />\n </div>\n <span class=\"v-dialog-edge\" data-edge=\"bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-dialog-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-dialog {\n /*\n * The token is declared on the element rather than written as a `var()` fallback. A custom\n * property inherits, and a dialog opened from inside another one is its DOM descendant:\n * with a fallback, an alert confirming something in an 800px dialog would read that\n * dialog's inline width and open 800px wide itself, with no error anywhere.\n */\n --dialog-width: var(--vectis-control-size-dialog-width);\n inline-size: var(--dialog-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(100dvb - 2 * var(--vectis-space-4));\n /*\n * Restore the modal's automatic margins in the component layer because the reset removes\n * native dialog centring.\n */\n margin: auto;\n /* The header and footer stay put while only the body scrolls, and hiding the\n overflow is what keeps the content inside the rounded corners. */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * An indispensable guard. The browser hides a closed dialog with a rule of its own, but any\n * display we declare above beats it; so without this, a closed dialog would sit in the page\n * at the top left, swallowing clicks.\n */\n .v-dialog:not([open]) {\n display: none;\n }\n\n /* Opening a modal puts the focus on the dialog itself. No ring is drawn around the\n whole thing for that: the focus a reader needs to see is the one on the controls\n inside. */\n .v-dialog:focus-visible {\n outline: none;\n }\n\n .v-dialog-scroll {\n /*\n * The zero minimum is load-bearing: without it a flex item refuses to shrink below its\n * content, and nothing would ever scroll.\n */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n /* A wheel reaching the end of the content stops there rather than scrolling the page\n behind the modal. */\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n /*\n * This makes the box something its descendants can ask questions about; namely whether\n * there is content hidden above or below. It adds no containment of size.\n */\n container-type: scroll-state;\n }\n\n /*\n * The two sentinels: hairlines stuck to the top and the bottom of the scrolling area, and\n * descendants of it, which allows them to ask about its scroll state. Their negative margins\n * mean they occupy no space at all.\n */\n .v-dialog-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-dialog-edge[data-edge='top'] {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-dialog-edge[data-edge='bottom'] {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-dialog-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-dialog-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n /* The title and the subtitle are rendered by VTypography, which carries their type.\n Their classes remain as hooks, for a consumer's overrides and for the tests. */\n\n .v-dialog-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* The cross is a button, so it carries invisible padding of its own; pulling the\n row back by that amount is what makes the glyph line up with the header's edge\n rather than floating inside it. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-dialog-body {\n /* Short content is stretched to fill the space, so the footer stays at the bottom\n of the dialog rather than floating halfway up; long content keeps its natural\n height, overflows, and is what scrolls. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-dialog-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where it is missing the sentinels simply stay transparent, which is the intended fallback. */\n @container scroll-state(scrollable: top) {\n .v-dialog-edge[data-edge='top'] {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-dialog-edge[data-edge='bottom'] {\n background: var(--vectis-color-border);\n }\n }\n\n /*\n * The dialog animates on the way in and not on the way out. Since it is removed from the page\n * the moment it closes, there is nothing left to animate out; that is the price of the clean\n * slate the lazy rendering buys, and it also spares the extra declarations an exit animation\n * would need.\n */\n .v-dialog {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-dialog::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-dialog,\n .v-dialog::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-dialog {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -105,12 +105,14 @@ var T = [
|
|
|
105
105
|
}, 8, ["label", "onClick"]))])) : u("", !0)]),
|
|
106
106
|
f("div", k, [
|
|
107
107
|
o[4] ||= f("span", {
|
|
108
|
-
class: "v-dialog-edge
|
|
108
|
+
class: "v-dialog-edge",
|
|
109
|
+
"data-edge": "top",
|
|
109
110
|
"aria-hidden": "true"
|
|
110
111
|
}, null, -1),
|
|
111
112
|
f("div", A, [y(e.$slots, "default")]),
|
|
112
113
|
o[5] ||= f("span", {
|
|
113
|
-
class: "v-dialog-edge
|
|
114
|
+
class: "v-dialog-edge",
|
|
115
|
+
"data-edge": "bottom",
|
|
114
116
|
"aria-hidden": "true"
|
|
115
117
|
}, null, -1)
|
|
116
118
|
]),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VDialog.vue_vue_type_script_setup_true_lang.js","names":["$slots","_useModel","_openBlock","_createElementBlock","_Fragment","_renderSlot","_unref","_mergeProps","_createElementVNode","_createBlock","VTypography","_withCtx","_createTextVNode","_toDisplayString","_createCommentVNode","VIconButton","close","_createVNode","VIcon","closeIcon","VIcon_default","VTypography_default","VIconButton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","VDialog_vue_vue_type_script_setup_true_lang_default","\"open\"","\"openModifiers\"","__expose","props","__props","m","useMessages","resolvedCloseLabel","computed","open","useModel","titleId","useId","subtitleId","close$1","useModalDialog","triggerProps","show","dialogEl","openBlock","createElementBlock","Fragment","renderSlot","_ctx","unref","rendered","mergeProps","rootAttrs","cssSize","_cache","args","onClose","onCancel","onPointerdown","onBackdropClick","createElementVNode","createBlock","withCtx","createTextVNode","toDisplayString","createCommentVNode","createVNode","close"],"sources":["../../../src/components/VDialog/VDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/** A native modal `<dialog>`, its model bridged by useModalDialog. */\n\nimport { computed, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** How assistive technology announces the dialog. */\nexport type DialogRole = 'dialog' | 'alertdialog'\n\ninterface DialogProps {\n /**\n * The title of the dialog, which also names it for assistive technology. It is\n * ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, explaining what the dialog is asking. */\n subtitle?: string\n /**\n * How wide the dialog is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-dialog-width` token, 400px by default.\n */\n width?: number | string\n /**\n * What kind of dialog this is. `alertdialog` is for one that must be answered\n * explicitly, and it makes screen readers announce it more insistently. VDialogAlert is\n * exactly that.\n */\n role?: DialogRole\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the\n * backdrop and whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the dialog from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the dialog. Note that refusing Escape while the\n * backdrop still closes cannot be expressed natively, so both routes are then\n * allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n role: 'dialog',\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the dialog is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\n/** What the trigger has to carry: the click that opens the dialog, and the fact that it does. */\nexport type DialogTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n}\n\ndefineSlots<{\n /** The body of the dialog. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross: a menu, a full-screen toggle. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the dialog. */\n footer?(): unknown\n /**\n * The button that opens the dialog. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the dialog itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the dialog, exactly as setting `open` does. The opening lands on the next tick,\n * once the element has been rendered: `el` is still null right after the call, and the\n * promise returned settles once the dialog is showing.\n */\n show,\n /**\n * Closes the dialog. It closes unconditionally, `persistentEscape` and\n * `persistentBackdrop` governing only the two routes the reader can take.\n */\n close,\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-dialog\"\n :style=\"{ '--dialog-width': cssSize(width) }\"\n :role=\"role === 'alertdialog' ? 'alertdialog' : undefined\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n The header and the footer stay put while only the body scrolls, so the scrollbar\n is confined to the body and never runs along them.\n\n The lines that appear under the header and above the footer once the body is\n scrolled are drawn by two sentinels stuck INSIDE the scrolling area, and not by\n the header and footer themselves: a container query can only style what is inside\n the container it asks about, never a sibling of it.\n -->\n <header class=\"v-dialog-header\">\n <slot name=\"header\">\n <div class=\"v-dialog-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-dialog-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-dialog-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-dialog-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-dialog-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-dialog-scroll\">\n <span class=\"v-dialog-edge v-dialog-edge--top\" aria-hidden=\"true\" />\n <div class=\"v-dialog-body\">\n <slot />\n </div>\n <span class=\"v-dialog-edge v-dialog-edge--bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-dialog-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-dialog {\n /*\n * The token is declared on the element rather than written as a `var()` fallback. A custom\n * property inherits, and a dialog opened from inside another one is its DOM descendant:\n * with a fallback, an alert confirming something in an 800px dialog would read that\n * dialog's inline width and open 800px wide itself, with no error anywhere.\n */\n --dialog-width: var(--vectis-control-size-dialog-width);\n inline-size: var(--dialog-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(100dvb - 2 * var(--vectis-space-4));\n /*\n * Restore the modal's automatic margins in the component layer because the reset removes\n * native dialog centring.\n */\n margin: auto;\n /* The header and footer stay put while only the body scrolls, and hiding the\n overflow is what keeps the content inside the rounded corners. */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * An indispensable guard. The browser hides a closed dialog with a rule of its own, but any\n * display we declare above beats it; so without this, a closed dialog would sit in the page\n * at the top left, swallowing clicks.\n */\n .v-dialog:not([open]) {\n display: none;\n }\n\n /* Opening a modal puts the focus on the dialog itself. No ring is drawn around the\n whole thing for that: the focus a reader needs to see is the one on the controls\n inside. */\n .v-dialog:focus-visible {\n outline: none;\n }\n\n .v-dialog-scroll {\n /*\n * The zero minimum is load-bearing: without it a flex item refuses to shrink below its\n * content, and nothing would ever scroll.\n */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n /* A wheel reaching the end of the content stops there rather than scrolling the page\n behind the modal. */\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n /*\n * This makes the box something its descendants can ask questions about; namely whether\n * there is content hidden above or below. It adds no containment of size.\n */\n container-type: scroll-state;\n }\n\n /*\n * The two sentinels: hairlines stuck to the top and the bottom of the scrolling area, and\n * descendants of it, which allows them to ask about its scroll state. Their negative margins\n * mean they occupy no space at all.\n */\n .v-dialog-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-dialog-edge--top {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-dialog-edge--bottom {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-dialog-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-dialog-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n /* The title and the subtitle are rendered by VTypography, which carries their type.\n Their classes remain as hooks, for a consumer's overrides and for the tests. */\n\n .v-dialog-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* The cross is a button, so it carries invisible padding of its own; pulling the\n row back by that amount is what makes the glyph line up with the header's edge\n rather than floating inside it. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-dialog-body {\n /* Short content is stretched to fill the space, so the footer stays at the bottom\n of the dialog rather than floating halfway up; long content keeps its natural\n height, overflows, and is what scrolls. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-dialog-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where it is missing the sentinels simply stay transparent, which is the intended fallback. */\n @container scroll-state(scrollable: top) {\n .v-dialog-edge--top {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-dialog-edge--bottom {\n background: var(--vectis-color-border);\n }\n }\n\n /*\n * The dialog animates on the way in and not on the way out. Since it is removed from the page\n * the moment it closes, there is nothing left to animate out; that is the price of the clean\n * slate the lazy rendering buys, and it also spares the extra declarations an exit animation\n * would need.\n */\n .v-dialog {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-dialog::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-dialog,\n .v-dialog::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-dialog {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VDialog.vue_vue_type_script_setup_true_lang.js","names":["$slots","_useModel","_openBlock","_createElementBlock","_Fragment","_renderSlot","_unref","_mergeProps","_createElementVNode","_createBlock","VTypography","_withCtx","_createTextVNode","_toDisplayString","_createCommentVNode","VIconButton","close","_createVNode","VIcon","closeIcon","VIcon_default","VTypography_default","VIconButton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","VDialog_vue_vue_type_script_setup_true_lang_default","\"open\"","\"openModifiers\"","__expose","props","__props","m","useMessages","resolvedCloseLabel","computed","open","useModel","titleId","useId","subtitleId","close$1","useModalDialog","triggerProps","show","dialogEl","openBlock","createElementBlock","Fragment","renderSlot","_ctx","unref","rendered","mergeProps","rootAttrs","cssSize","_cache","args","onClose","onCancel","onPointerdown","onBackdropClick","createElementVNode","createBlock","withCtx","createTextVNode","toDisplayString","createCommentVNode","createVNode","close"],"sources":["../../../src/components/VDialog/VDialog.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/** A native modal `<dialog>`, its model bridged by useModalDialog. */\n\nimport { computed, useId } from 'vue'\n\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** How assistive technology announces the dialog. */\nexport type DialogRole = 'dialog' | 'alertdialog'\n\ninterface DialogProps {\n /**\n * The title of the dialog, which also names it for assistive technology. It is\n * ignored when the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title, explaining what the dialog is asking. */\n subtitle?: string\n /**\n * How wide the dialog is: a number is read as pixels, a string as any CSS length. Left out,\n * it takes the `--vectis-control-size-dialog-width` token, 400px by default.\n */\n width?: number | string\n /**\n * What kind of dialog this is. `alertdialog` is for one that must be answered\n * explicitly, and it makes screen readers announce it more insistently. VDialogAlert is\n * exactly that.\n */\n role?: DialogRole\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the\n * backdrop and whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the dialog from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the dialog. Note that refusing Escape while the\n * backdrop still closes cannot be expressed natively, so both routes are then\n * allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DialogProps>(), {\n title: undefined,\n subtitle: undefined,\n width: undefined,\n role: 'dialog',\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the dialog is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\n/** What the trigger has to carry: the click that opens the dialog, and the fact that it does. */\nexport type DialogTriggerProps = {\n onClick: () => void\n 'aria-haspopup': 'dialog'\n}\n\ndefineSlots<{\n /** The body of the dialog. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross: a menu, a full-screen toggle. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the dialog. */\n footer?(): unknown\n /**\n * The button that opens the dialog. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the dialog itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the dialog, exactly as setting `open` does. The opening lands on the next tick,\n * once the element has been rendered: `el` is still null right after the call, and the\n * promise returned settles once the dialog is showing.\n */\n show,\n /**\n * Closes the dialog. It closes unconditionally, `persistentEscape` and\n * `persistentBackdrop` governing only the two routes the reader can take.\n */\n close,\n /** The `<dialog>` element. It is null while closed: each opening builds a fresh one. */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-dialog\"\n :style=\"{ '--dialog-width': cssSize(width) }\"\n :role=\"role === 'alertdialog' ? 'alertdialog' : undefined\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n The header and the footer stay put while only the body scrolls, so the scrollbar\n is confined to the body and never runs along them.\n\n The lines that appear under the header and above the footer once the body is\n scrolled are drawn by two sentinels stuck INSIDE the scrolling area, and not by\n the header and footer themselves: a container query can only style what is inside\n the container it asks about, never a sibling of it.\n -->\n <header class=\"v-dialog-header\">\n <slot name=\"header\">\n <div class=\"v-dialog-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-dialog-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-dialog-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-dialog-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-dialog-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-dialog-scroll\">\n <span class=\"v-dialog-edge\" data-edge=\"top\" aria-hidden=\"true\" />\n <div class=\"v-dialog-body\">\n <slot />\n </div>\n <span class=\"v-dialog-edge\" data-edge=\"bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-dialog-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-dialog {\n /*\n * The token is declared on the element rather than written as a `var()` fallback. A custom\n * property inherits, and a dialog opened from inside another one is its DOM descendant:\n * with a fallback, an alert confirming something in an 800px dialog would read that\n * dialog's inline width and open 800px wide itself, with no error anywhere.\n */\n --dialog-width: var(--vectis-control-size-dialog-width);\n inline-size: var(--dialog-width);\n max-inline-size: calc(100dvi - 2 * var(--vectis-space-4));\n max-block-size: calc(100dvb - 2 * var(--vectis-space-4));\n /*\n * Restore the modal's automatic margins in the component layer because the reset removes\n * native dialog centring.\n */\n margin: auto;\n /* The header and footer stay put while only the body scrolls, and hiding the\n overflow is what keeps the content inside the rounded corners. */\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n border-radius: var(--vectis-radius-overlay);\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n }\n\n /*\n * An indispensable guard. The browser hides a closed dialog with a rule of its own, but any\n * display we declare above beats it; so without this, a closed dialog would sit in the page\n * at the top left, swallowing clicks.\n */\n .v-dialog:not([open]) {\n display: none;\n }\n\n /* Opening a modal puts the focus on the dialog itself. No ring is drawn around the\n whole thing for that: the focus a reader needs to see is the one on the controls\n inside. */\n .v-dialog:focus-visible {\n outline: none;\n }\n\n .v-dialog-scroll {\n /*\n * The zero minimum is load-bearing: without it a flex item refuses to shrink below its\n * content, and nothing would ever scroll.\n */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n /* A wheel reaching the end of the content stops there rather than scrolling the page\n behind the modal. */\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n /*\n * This makes the box something its descendants can ask questions about; namely whether\n * there is content hidden above or below. It adds no containment of size.\n */\n container-type: scroll-state;\n }\n\n /*\n * The two sentinels: hairlines stuck to the top and the bottom of the scrolling area, and\n * descendants of it, which allows them to ask about its scroll state. Their negative margins\n * mean they occupy no space at all.\n */\n .v-dialog-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-dialog-edge[data-edge='top'] {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-dialog-edge[data-edge='bottom'] {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-dialog-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-dialog-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n /* The title and the subtitle are rendered by VTypography, which carries their type.\n Their classes remain as hooks, for a consumer's overrides and for the tests. */\n\n .v-dialog-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* The cross is a button, so it carries invisible padding of its own; pulling the\n row back by that amount is what makes the glyph line up with the header's edge\n rather than floating inside it. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-dialog-body {\n /* Short content is stretched to fill the space, so the footer stays at the bottom\n of the dialog rather than floating halfway up; long content keeps its natural\n height, overflows, and is what scrolls. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-dialog-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where it is missing the sentinels simply stay transparent, which is the intended fallback. */\n @container scroll-state(scrollable: top) {\n .v-dialog-edge[data-edge='top'] {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-dialog-edge[data-edge='bottom'] {\n background: var(--vectis-color-border);\n }\n }\n\n /*\n * The dialog animates on the way in and not on the way out. Since it is removed from the page\n * the moment it closes, there is nothing left to animate out; that is the price of the clean\n * slate the lazy rendering buys, and it also spares the extra declarations an exit animation\n * would need.\n */\n .v-dialog {\n transition:\n opacity var(--vectis-duration-base) var(--vectis-ease-default),\n transform var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open] {\n opacity: 0;\n transform: scale(0.97);\n }\n }\n\n .v-dialog::backdrop {\n background: var(--vectis-color-backdrop);\n transition: opacity var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @starting-style {\n .v-dialog[open]::backdrop {\n opacity: 0;\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-dialog,\n .v-dialog::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-dialog {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-drawer{--drawer-extent:var(--vectis-control-size-drawer-md);--drawer-offset:100% 0;background:var(--vectis-color-surface-overlay);block-size:auto;max-block-size:none;inline-size:auto;max-inline-size:none;color:var(--vectis-color-text);box-shadow:var(--vectis-shadow-xl);font-family:var(--vectis-text-family);transition:translate var(--vectis-duration-slow) var(--vectis-ease-out), display var(--vectis-duration-slow) allow-discrete, overlay var(--vectis-duration-slow) allow-discrete;border:none;flex-direction:column;margin:0;padding:0;display:flex;position:fixed;inset:0;overflow:hidden}.v-drawer[data-size=sm]{--drawer-extent:var(--vectis-control-size-drawer-sm)}.v-drawer[data-size=lg]{--drawer-extent:var(--vectis-control-size-drawer-lg)}.v-drawer[data-side=start],.v-drawer[data-side=end]{inline-size:var(--drawer-extent);max-inline-size:calc(100dvi - var(--vectis-space-12))}.v-drawer[data-side=top],.v-drawer[data-side=bottom]{block-size:var(--drawer-extent);max-block-size:calc(100dvb - var(--vectis-space-12))}.v-drawer[data-side=start]{--drawer-offset:-100% 0;border-start-end-radius:var(--vectis-radius-overlay);border-end-end-radius:var(--vectis-radius-overlay);inset-inline-end:auto}.v-drawer[data-side=end]{border-start-start-radius:var(--vectis-radius-overlay);border-end-start-radius:var(--vectis-radius-overlay);inset-inline-start:auto}.v-drawer[data-side=start]:dir(rtl){--drawer-offset:100% 0}.v-drawer[data-side=end]:dir(rtl){--drawer-offset:-100% 0}.v-drawer[data-side=top]{--drawer-offset:0 -100%;border-bottom-left-radius:var(--vectis-radius-overlay);border-bottom-right-radius:var(--vectis-radius-overlay);bottom:auto}.v-drawer[data-side=bottom]{--drawer-offset:0 100%;border-top-left-radius:var(--vectis-radius-overlay);border-top-right-radius:var(--vectis-radius-overlay);top:auto}.v-drawer:not([open]){translate:var(--drawer-offset);pointer-events:none;transition-timing-function:var(--vectis-ease-in);display:none}@starting-style{.v-drawer[open]{translate:var(--drawer-offset)}}.v-drawer::backdrop{background:var(--vectis-color-backdrop);transition:opacity var(--vectis-duration-slow) var(--vectis-ease-default), display var(--vectis-duration-slow) allow-discrete, overlay var(--vectis-duration-slow) allow-discrete}.v-drawer:not([open])::backdrop{opacity:0}@starting-style{.v-drawer[open]::backdrop{opacity:0}}.v-drawer:focus-visible{outline:none}.v-drawer-scroll{overscroll-behavior:contain;flex-direction:column;flex:auto;min-block-size:0;display:flex;overflow-y:auto;container-type:scroll-state}.v-drawer-edge{background:0 0;flex:none;block-size:1px;position:sticky}.v-drawer-edge
|
|
2
|
+
@layer vectis.components{.v-drawer{--drawer-extent:var(--vectis-control-size-drawer-md);--drawer-offset:100% 0;background:var(--vectis-color-surface-overlay);block-size:auto;max-block-size:none;inline-size:auto;max-inline-size:none;color:var(--vectis-color-text);box-shadow:var(--vectis-shadow-xl);font-family:var(--vectis-text-family);transition:translate var(--vectis-duration-slow) var(--vectis-ease-out), display var(--vectis-duration-slow) allow-discrete, overlay var(--vectis-duration-slow) allow-discrete;border:none;flex-direction:column;margin:0;padding:0;display:flex;position:fixed;inset:0;overflow:hidden}.v-drawer[data-size=sm]{--drawer-extent:var(--vectis-control-size-drawer-sm)}.v-drawer[data-size=lg]{--drawer-extent:var(--vectis-control-size-drawer-lg)}.v-drawer[data-side=start],.v-drawer[data-side=end]{inline-size:var(--drawer-extent);max-inline-size:calc(100dvi - var(--vectis-space-12))}.v-drawer[data-side=top],.v-drawer[data-side=bottom]{block-size:var(--drawer-extent);max-block-size:calc(100dvb - var(--vectis-space-12))}.v-drawer[data-side=start]{--drawer-offset:-100% 0;border-start-end-radius:var(--vectis-radius-overlay);border-end-end-radius:var(--vectis-radius-overlay);inset-inline-end:auto}.v-drawer[data-side=end]{border-start-start-radius:var(--vectis-radius-overlay);border-end-start-radius:var(--vectis-radius-overlay);inset-inline-start:auto}.v-drawer[data-side=start]:dir(rtl){--drawer-offset:100% 0}.v-drawer[data-side=end]:dir(rtl){--drawer-offset:-100% 0}.v-drawer[data-side=top]{--drawer-offset:0 -100%;border-bottom-left-radius:var(--vectis-radius-overlay);border-bottom-right-radius:var(--vectis-radius-overlay);bottom:auto}.v-drawer[data-side=bottom]{--drawer-offset:0 100%;border-top-left-radius:var(--vectis-radius-overlay);border-top-right-radius:var(--vectis-radius-overlay);top:auto}.v-drawer:not([open]){translate:var(--drawer-offset);pointer-events:none;transition-timing-function:var(--vectis-ease-in);display:none}@starting-style{.v-drawer[open]{translate:var(--drawer-offset)}}.v-drawer::backdrop{background:var(--vectis-color-backdrop);transition:opacity var(--vectis-duration-slow) var(--vectis-ease-default), display var(--vectis-duration-slow) allow-discrete, overlay var(--vectis-duration-slow) allow-discrete}.v-drawer:not([open])::backdrop{opacity:0}@starting-style{.v-drawer[open]::backdrop{opacity:0}}.v-drawer:focus-visible{outline:none}.v-drawer-scroll{overscroll-behavior:contain;flex-direction:column;flex:auto;min-block-size:0;display:flex;overflow-y:auto;container-type:scroll-state}.v-drawer-edge{background:0 0;flex:none;block-size:1px;position:sticky}.v-drawer-edge[data-edge=top]{margin-block-end:-1px;inset-block-start:0}.v-drawer-edge[data-edge=bottom]{margin-block-start:-1px;inset-block-end:0}.v-drawer-header{justify-content:space-between;align-items:flex-start;gap:var(--vectis-space-4);padding:var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);flex:none;display:flex}.v-drawer-titles{flex-direction:column;min-inline-size:0;display:flex}.v-drawer-header-actions{align-items:center;gap:var(--vectis-space-1);margin-block-start:calc(-1 * var(--vectis-space-1));margin-inline-end:calc(-1 * var(--vectis-space-2));display:flex}.v-drawer-body{padding:var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);font-size:var(--vectis-text-body-md-size);line-height:var(--vectis-text-body-md-leading);flex:1 0 auto}.v-drawer-footer{justify-content:flex-end;align-items:center;gap:var(--vectis-space-3);padding:var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);flex-wrap:wrap;flex:none;display:flex}@container scroll-state(scrollable:top){.v-drawer-edge[data-edge=top]{background:var(--vectis-color-border)}}@container scroll-state(scrollable:bottom){.v-drawer-edge[data-edge=bottom]{background:var(--vectis-color-border)}}@media (prefers-reduced-motion:reduce){.v-drawer,.v-drawer::backdrop{transition:none}}@media (forced-colors:active){.v-drawer{outline:1px solid canvastext}}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VDrawer.js","names":["_sfc_main","VDrawer_vue_vue_type_script_setup_true_lang_default","VDrawer_default"],"sources":["../../../src/components/VDrawer/VDrawer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A native modal `<dialog>` pinned to one edge of the viewport, its model bridged by\n * useModalDialog. It stays rendered through its exit, so it slides out as it slid in.\n */\n\nimport { computed, useId } from 'vue'\n\nimport type { DialogTriggerProps } from '../VDialog/VDialog.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** The edge the drawer comes from. `start` and `end` follow the writing direction. */\nexport type DrawerSide = 'start' | 'end' | 'top' | 'bottom'\n\n/** The width of a drawer on a side, or the height of one at the top or bottom. */\nexport type DrawerSize = 'sm' | 'md' | 'lg'\n\ninterface DrawerProps {\n /**\n * The title of the drawer, which also names it for assistive technology. It is ignored when\n * the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title. */\n subtitle?: string\n /** The edge the drawer comes from. `start` and `end` follow the writing direction. */\n side?: DrawerSide\n /**\n * How far the drawer reaches into the page: its width on a side, its height at the top or\n * bottom. Each step reads a `--vectis-control-size-drawer-*` token.\n */\n size?: DrawerSize\n /**\n * A length replacing `size`: a number is read as pixels, a string as any CSS length. The\n * drawer always leaves a strip of the page uncovered, which a click closes it through.\n */\n extent?: number | string\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the backdrop and\n * whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the drawer from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the drawer. Refusing Escape while the backdrop still\n * closes cannot be expressed natively, so both routes are then allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DrawerProps>(), {\n title: undefined,\n subtitle: undefined,\n side: 'end',\n size: 'md',\n extent: undefined,\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the drawer is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** The body of the drawer. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the drawer. */\n footer?(): unknown\n /**\n * The button that opens the drawer. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the drawer itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n exitTransition: true,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the drawer, exactly as setting `open` does. The opening lands on the next tick: the\n * promise returned settles once the drawer is showing.\n */\n show,\n /**\n * Closes the drawer. It closes unconditionally, `persistentEscape` and `persistentBackdrop`\n * governing only the two routes the reader can take.\n */\n close,\n /**\n * The `<dialog>` element. It is null while closed, once its exit has played: each opening\n * from there builds a fresh one.\n */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-drawer\"\n :data-side=\"side\"\n :data-size=\"size\"\n :style=\"{ '--drawer-extent': cssSize(extent) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n As in VDialog, only the body scrolls, and the lines under the header and above the footer\n are drawn by sentinels stuck inside the scrolling area, which a scroll-state query reaches.\n -->\n <header class=\"v-drawer-header\">\n <slot name=\"header\">\n <div class=\"v-drawer-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-drawer-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-drawer-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-drawer-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-drawer-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-drawer-scroll\">\n <span class=\"v-drawer-edge v-drawer-edge--top\" aria-hidden=\"true\" />\n <div class=\"v-drawer-body\">\n <slot />\n </div>\n <span class=\"v-drawer-edge v-drawer-edge--bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-drawer-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-drawer {\n /*\n * Declared on the element rather than as a `var()` fallback: a drawer opened from inside\n * another is its DOM descendant, and would otherwise inherit that drawer's inline extent.\n */\n --drawer-extent: var(--vectis-control-size-drawer-md);\n /* Where the drawer waits off screen, before it enters and after it leaves. */\n --drawer-offset: 100% 0;\n /*\n * Fixed explicitly rather than left to the modal's UA style: an engine without `overlay`\n * takes the drawer out of the top layer the moment it closes, and it must not fall back\n * into the page while it slides out.\n */\n position: fixed;\n inset: 0;\n margin: 0;\n inline-size: auto;\n block-size: auto;\n max-inline-size: none;\n max-block-size: none;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n /*\n * `display` and `overlay` switch at the END of the exit, which keeps the closed drawer\n * painted, and in the top layer, while it slides out.\n */\n transition:\n translate var(--vectis-duration-slow) var(--vectis-ease-out),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer[data-size='sm'] {\n --drawer-extent: var(--vectis-control-size-drawer-sm);\n }\n\n .v-drawer[data-size='lg'] {\n --drawer-extent: var(--vectis-control-size-drawer-lg);\n }\n\n /* The uncovered strip keeps the backdrop within reach, to see the page and to close. */\n .v-drawer[data-side='start'],\n .v-drawer[data-side='end'] {\n inline-size: var(--drawer-extent);\n max-inline-size: calc(100dvi - var(--vectis-space-12));\n }\n\n .v-drawer[data-side='top'],\n .v-drawer[data-side='bottom'] {\n block-size: var(--drawer-extent);\n max-block-size: calc(100dvb - var(--vectis-space-12));\n }\n\n /* The corners against the viewport stay square; the two facing the page are rounded. */\n .v-drawer[data-side='start'] {\n --drawer-offset: -100% 0;\n inset-inline-end: auto;\n border-start-end-radius: var(--vectis-radius-overlay);\n border-end-end-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='end'] {\n inset-inline-start: auto;\n border-start-start-radius: var(--vectis-radius-overlay);\n border-end-start-radius: var(--vectis-radius-overlay);\n }\n\n /* A translation is physical: in a right-to-left page the inline sides swap directions. */\n .v-drawer[data-side='start']:dir(rtl) {\n --drawer-offset: 100% 0;\n }\n\n .v-drawer[data-side='end']:dir(rtl) {\n --drawer-offset: -100% 0;\n }\n\n .v-drawer[data-side='top'] {\n --drawer-offset: 0 -100%;\n bottom: auto;\n border-bottom-left-radius: var(--vectis-radius-overlay);\n border-bottom-right-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='bottom'] {\n --drawer-offset: 0 100%;\n top: auto;\n border-top-left-radius: var(--vectis-radius-overlay);\n border-top-right-radius: var(--vectis-radius-overlay);\n }\n\n /*\n * An indispensable guard: the display declared above beats the browser's own rule hiding a\n * closed dialog. Through the exit, the transition holds `display` back, and a closed drawer\n * sliding away lets clicks through to the page it uncovers.\n */\n .v-drawer:not([open]) {\n display: none;\n translate: var(--drawer-offset);\n pointer-events: none;\n transition-timing-function: var(--vectis-ease-in);\n }\n\n @starting-style {\n .v-drawer[open] {\n translate: var(--drawer-offset);\n }\n }\n\n .v-drawer::backdrop {\n background: var(--vectis-color-backdrop);\n transition:\n opacity var(--vectis-duration-slow) var(--vectis-ease-default),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer:not([open])::backdrop {\n opacity: 0;\n }\n\n @starting-style {\n .v-drawer[open]::backdrop {\n opacity: 0;\n }\n }\n\n /* No ring around the whole drawer when opening focuses it: the controls inside show theirs. */\n .v-drawer:focus-visible {\n outline: none;\n }\n\n .v-drawer-scroll {\n /* The zero minimum lets the flex item shrink below its content, so that it scrolls. */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n container-type: scroll-state;\n }\n\n /* Negative margins make the sentinels occupy no space at all. */\n .v-drawer-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-drawer-edge--top {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-drawer-edge--bottom {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-drawer-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-drawer-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n .v-drawer-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* Pulled back by the cross's own padding, so its glyph lines up with the header's edge. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-drawer-body {\n /* Stretched when short, so the footer stays at the bottom of the drawer. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-drawer-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where scroll-state queries are missing, the sentinels simply stay transparent. */\n @container scroll-state(scrollable: top) {\n .v-drawer-edge--top {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-drawer-edge--bottom {\n background: var(--vectis-color-border);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-drawer,\n .v-drawer::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-drawer {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VDrawer.js","names":["_sfc_main","VDrawer_vue_vue_type_script_setup_true_lang_default","VDrawer_default"],"sources":["../../../src/components/VDrawer/VDrawer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A native modal `<dialog>` pinned to one edge of the viewport, its model bridged by\n * useModalDialog. It stays rendered through its exit, so it slides out as it slid in.\n */\n\nimport { computed, useId } from 'vue'\n\nimport type { DialogTriggerProps } from '../VDialog/VDialog.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** The edge the drawer comes from. `start` and `end` follow the writing direction. */\nexport type DrawerSide = 'start' | 'end' | 'top' | 'bottom'\n\n/** The width of a drawer on a side, or the height of one at the top or bottom. */\nexport type DrawerSize = 'sm' | 'md' | 'lg'\n\ninterface DrawerProps {\n /**\n * The title of the drawer, which also names it for assistive technology. It is ignored when\n * the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title. */\n subtitle?: string\n /** The edge the drawer comes from. `start` and `end` follow the writing direction. */\n side?: DrawerSide\n /**\n * How far the drawer reaches into the page: its width on a side, its height at the top or\n * bottom. Each step reads a `--vectis-control-size-drawer-*` token.\n */\n size?: DrawerSize\n /**\n * A length replacing `size`: a number is read as pixels, a string as any CSS length. The\n * drawer always leaves a strip of the page uncovered, which a click closes it through.\n */\n extent?: number | string\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the backdrop and\n * whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the drawer from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the drawer. Refusing Escape while the backdrop still\n * closes cannot be expressed natively, so both routes are then allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DrawerProps>(), {\n title: undefined,\n subtitle: undefined,\n side: 'end',\n size: 'md',\n extent: undefined,\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the drawer is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** The body of the drawer. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the drawer. */\n footer?(): unknown\n /**\n * The button that opens the drawer. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the drawer itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n exitTransition: true,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the drawer, exactly as setting `open` does. The opening lands on the next tick: the\n * promise returned settles once the drawer is showing.\n */\n show,\n /**\n * Closes the drawer. It closes unconditionally, `persistentEscape` and `persistentBackdrop`\n * governing only the two routes the reader can take.\n */\n close,\n /**\n * The `<dialog>` element. It is null while closed, once its exit has played: each opening\n * from there builds a fresh one.\n */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-drawer\"\n :data-side=\"side\"\n :data-size=\"size\"\n :style=\"{ '--drawer-extent': cssSize(extent) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n As in VDialog, only the body scrolls, and the lines under the header and above the footer\n are drawn by sentinels stuck inside the scrolling area, which a scroll-state query reaches.\n -->\n <header class=\"v-drawer-header\">\n <slot name=\"header\">\n <div class=\"v-drawer-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-drawer-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-drawer-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-drawer-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-drawer-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-drawer-scroll\">\n <span class=\"v-drawer-edge\" data-edge=\"top\" aria-hidden=\"true\" />\n <div class=\"v-drawer-body\">\n <slot />\n </div>\n <span class=\"v-drawer-edge\" data-edge=\"bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-drawer-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-drawer {\n /*\n * Declared on the element rather than as a `var()` fallback: a drawer opened from inside\n * another is its DOM descendant, and would otherwise inherit that drawer's inline extent.\n */\n --drawer-extent: var(--vectis-control-size-drawer-md);\n /* Where the drawer waits off screen, before it enters and after it leaves. */\n --drawer-offset: 100% 0;\n /*\n * Fixed explicitly rather than left to the modal's UA style: an engine without `overlay`\n * takes the drawer out of the top layer the moment it closes, and it must not fall back\n * into the page while it slides out.\n */\n position: fixed;\n inset: 0;\n margin: 0;\n inline-size: auto;\n block-size: auto;\n max-inline-size: none;\n max-block-size: none;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n /*\n * `display` and `overlay` switch at the END of the exit, which keeps the closed drawer\n * painted, and in the top layer, while it slides out.\n */\n transition:\n translate var(--vectis-duration-slow) var(--vectis-ease-out),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer[data-size='sm'] {\n --drawer-extent: var(--vectis-control-size-drawer-sm);\n }\n\n .v-drawer[data-size='lg'] {\n --drawer-extent: var(--vectis-control-size-drawer-lg);\n }\n\n /* The uncovered strip keeps the backdrop within reach, to see the page and to close. */\n .v-drawer[data-side='start'],\n .v-drawer[data-side='end'] {\n inline-size: var(--drawer-extent);\n max-inline-size: calc(100dvi - var(--vectis-space-12));\n }\n\n .v-drawer[data-side='top'],\n .v-drawer[data-side='bottom'] {\n block-size: var(--drawer-extent);\n max-block-size: calc(100dvb - var(--vectis-space-12));\n }\n\n /* The corners against the viewport stay square; the two facing the page are rounded. */\n .v-drawer[data-side='start'] {\n --drawer-offset: -100% 0;\n inset-inline-end: auto;\n border-start-end-radius: var(--vectis-radius-overlay);\n border-end-end-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='end'] {\n inset-inline-start: auto;\n border-start-start-radius: var(--vectis-radius-overlay);\n border-end-start-radius: var(--vectis-radius-overlay);\n }\n\n /* A translation is physical: in a right-to-left page the inline sides swap directions. */\n .v-drawer[data-side='start']:dir(rtl) {\n --drawer-offset: 100% 0;\n }\n\n .v-drawer[data-side='end']:dir(rtl) {\n --drawer-offset: -100% 0;\n }\n\n .v-drawer[data-side='top'] {\n --drawer-offset: 0 -100%;\n bottom: auto;\n border-bottom-left-radius: var(--vectis-radius-overlay);\n border-bottom-right-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='bottom'] {\n --drawer-offset: 0 100%;\n top: auto;\n border-top-left-radius: var(--vectis-radius-overlay);\n border-top-right-radius: var(--vectis-radius-overlay);\n }\n\n /*\n * An indispensable guard: the display declared above beats the browser's own rule hiding a\n * closed dialog. Through the exit, the transition holds `display` back, and a closed drawer\n * sliding away lets clicks through to the page it uncovers.\n */\n .v-drawer:not([open]) {\n display: none;\n translate: var(--drawer-offset);\n pointer-events: none;\n transition-timing-function: var(--vectis-ease-in);\n }\n\n @starting-style {\n .v-drawer[open] {\n translate: var(--drawer-offset);\n }\n }\n\n .v-drawer::backdrop {\n background: var(--vectis-color-backdrop);\n transition:\n opacity var(--vectis-duration-slow) var(--vectis-ease-default),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer:not([open])::backdrop {\n opacity: 0;\n }\n\n @starting-style {\n .v-drawer[open]::backdrop {\n opacity: 0;\n }\n }\n\n /* No ring around the whole drawer when opening focuses it: the controls inside show theirs. */\n .v-drawer:focus-visible {\n outline: none;\n }\n\n .v-drawer-scroll {\n /* The zero minimum lets the flex item shrink below its content, so that it scrolls. */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n container-type: scroll-state;\n }\n\n /* Negative margins make the sentinels occupy no space at all. */\n .v-drawer-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-drawer-edge[data-edge='top'] {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-drawer-edge[data-edge='bottom'] {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-drawer-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-drawer-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n .v-drawer-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* Pulled back by the cross's own padding, so its glyph lines up with the header's edge. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-drawer-body {\n /* Stretched when short, so the footer stays at the bottom of the drawer. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-drawer-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where scroll-state queries are missing, the sentinels simply stay transparent. */\n @container scroll-state(scrollable: top) {\n .v-drawer-edge[data-edge='top'] {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-drawer-edge[data-edge='bottom'] {\n background: var(--vectis-color-border);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-drawer,\n .v-drawer::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-drawer {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
@@ -109,12 +109,14 @@ var T = [
|
|
|
109
109
|
}, 8, ["label", "onClick"]))])) : u("", !0)]),
|
|
110
110
|
f("div", k, [
|
|
111
111
|
o[4] ||= f("span", {
|
|
112
|
-
class: "v-drawer-edge
|
|
112
|
+
class: "v-drawer-edge",
|
|
113
|
+
"data-edge": "top",
|
|
113
114
|
"aria-hidden": "true"
|
|
114
115
|
}, null, -1),
|
|
115
116
|
f("div", A, [y(e.$slots, "default")]),
|
|
116
117
|
o[5] ||= f("span", {
|
|
117
|
-
class: "v-drawer-edge
|
|
118
|
+
class: "v-drawer-edge",
|
|
119
|
+
"data-edge": "bottom",
|
|
118
120
|
"aria-hidden": "true"
|
|
119
121
|
}, null, -1)
|
|
120
122
|
]),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VDrawer.vue_vue_type_script_setup_true_lang.js","names":["$slots","_useModel","_openBlock","_createElementBlock","_Fragment","_renderSlot","_unref","_mergeProps","_createElementVNode","_createBlock","VTypography","_withCtx","_createTextVNode","_toDisplayString","_createCommentVNode","VIconButton","close","_createVNode","VIcon","closeIcon","VIcon_default","VTypography_default","VIconButton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","VDrawer_vue_vue_type_script_setup_true_lang_default","\"open\"","\"openModifiers\"","__expose","props","__props","m","useMessages","resolvedCloseLabel","computed","open","useModel","titleId","useId","subtitleId","close$1","useModalDialog","triggerProps","show","dialogEl","openBlock","createElementBlock","Fragment","renderSlot","_ctx","unref","rendered","mergeProps","rootAttrs","cssSize","_cache","args","onClose","onCancel","onPointerdown","onBackdropClick","createElementVNode","createBlock","withCtx","createTextVNode","toDisplayString","createCommentVNode","createVNode","close"],"sources":["../../../src/components/VDrawer/VDrawer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A native modal `<dialog>` pinned to one edge of the viewport, its model bridged by\n * useModalDialog. It stays rendered through its exit, so it slides out as it slid in.\n */\n\nimport { computed, useId } from 'vue'\n\nimport type { DialogTriggerProps } from '../VDialog/VDialog.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** The edge the drawer comes from. `start` and `end` follow the writing direction. */\nexport type DrawerSide = 'start' | 'end' | 'top' | 'bottom'\n\n/** The width of a drawer on a side, or the height of one at the top or bottom. */\nexport type DrawerSize = 'sm' | 'md' | 'lg'\n\ninterface DrawerProps {\n /**\n * The title of the drawer, which also names it for assistive technology. It is ignored when\n * the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title. */\n subtitle?: string\n /** The edge the drawer comes from. `start` and `end` follow the writing direction. */\n side?: DrawerSide\n /**\n * How far the drawer reaches into the page: its width on a side, its height at the top or\n * bottom. Each step reads a `--vectis-control-size-drawer-*` token.\n */\n size?: DrawerSize\n /**\n * A length replacing `size`: a number is read as pixels, a string as any CSS length. The\n * drawer always leaves a strip of the page uncovered, which a click closes it through.\n */\n extent?: number | string\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the backdrop and\n * whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the drawer from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the drawer. Refusing Escape while the backdrop still\n * closes cannot be expressed natively, so both routes are then allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DrawerProps>(), {\n title: undefined,\n subtitle: undefined,\n side: 'end',\n size: 'md',\n extent: undefined,\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the drawer is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** The body of the drawer. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the drawer. */\n footer?(): unknown\n /**\n * The button that opens the drawer. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the drawer itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n exitTransition: true,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the drawer, exactly as setting `open` does. The opening lands on the next tick: the\n * promise returned settles once the drawer is showing.\n */\n show,\n /**\n * Closes the drawer. It closes unconditionally, `persistentEscape` and `persistentBackdrop`\n * governing only the two routes the reader can take.\n */\n close,\n /**\n * The `<dialog>` element. It is null while closed, once its exit has played: each opening\n * from there builds a fresh one.\n */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-drawer\"\n :data-side=\"side\"\n :data-size=\"size\"\n :style=\"{ '--drawer-extent': cssSize(extent) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n As in VDialog, only the body scrolls, and the lines under the header and above the footer\n are drawn by sentinels stuck inside the scrolling area, which a scroll-state query reaches.\n -->\n <header class=\"v-drawer-header\">\n <slot name=\"header\">\n <div class=\"v-drawer-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-drawer-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-drawer-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-drawer-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-drawer-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-drawer-scroll\">\n <span class=\"v-drawer-edge v-drawer-edge--top\" aria-hidden=\"true\" />\n <div class=\"v-drawer-body\">\n <slot />\n </div>\n <span class=\"v-drawer-edge v-drawer-edge--bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-drawer-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-drawer {\n /*\n * Declared on the element rather than as a `var()` fallback: a drawer opened from inside\n * another is its DOM descendant, and would otherwise inherit that drawer's inline extent.\n */\n --drawer-extent: var(--vectis-control-size-drawer-md);\n /* Where the drawer waits off screen, before it enters and after it leaves. */\n --drawer-offset: 100% 0;\n /*\n * Fixed explicitly rather than left to the modal's UA style: an engine without `overlay`\n * takes the drawer out of the top layer the moment it closes, and it must not fall back\n * into the page while it slides out.\n */\n position: fixed;\n inset: 0;\n margin: 0;\n inline-size: auto;\n block-size: auto;\n max-inline-size: none;\n max-block-size: none;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n /*\n * `display` and `overlay` switch at the END of the exit, which keeps the closed drawer\n * painted, and in the top layer, while it slides out.\n */\n transition:\n translate var(--vectis-duration-slow) var(--vectis-ease-out),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer[data-size='sm'] {\n --drawer-extent: var(--vectis-control-size-drawer-sm);\n }\n\n .v-drawer[data-size='lg'] {\n --drawer-extent: var(--vectis-control-size-drawer-lg);\n }\n\n /* The uncovered strip keeps the backdrop within reach, to see the page and to close. */\n .v-drawer[data-side='start'],\n .v-drawer[data-side='end'] {\n inline-size: var(--drawer-extent);\n max-inline-size: calc(100dvi - var(--vectis-space-12));\n }\n\n .v-drawer[data-side='top'],\n .v-drawer[data-side='bottom'] {\n block-size: var(--drawer-extent);\n max-block-size: calc(100dvb - var(--vectis-space-12));\n }\n\n /* The corners against the viewport stay square; the two facing the page are rounded. */\n .v-drawer[data-side='start'] {\n --drawer-offset: -100% 0;\n inset-inline-end: auto;\n border-start-end-radius: var(--vectis-radius-overlay);\n border-end-end-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='end'] {\n inset-inline-start: auto;\n border-start-start-radius: var(--vectis-radius-overlay);\n border-end-start-radius: var(--vectis-radius-overlay);\n }\n\n /* A translation is physical: in a right-to-left page the inline sides swap directions. */\n .v-drawer[data-side='start']:dir(rtl) {\n --drawer-offset: 100% 0;\n }\n\n .v-drawer[data-side='end']:dir(rtl) {\n --drawer-offset: -100% 0;\n }\n\n .v-drawer[data-side='top'] {\n --drawer-offset: 0 -100%;\n bottom: auto;\n border-bottom-left-radius: var(--vectis-radius-overlay);\n border-bottom-right-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='bottom'] {\n --drawer-offset: 0 100%;\n top: auto;\n border-top-left-radius: var(--vectis-radius-overlay);\n border-top-right-radius: var(--vectis-radius-overlay);\n }\n\n /*\n * An indispensable guard: the display declared above beats the browser's own rule hiding a\n * closed dialog. Through the exit, the transition holds `display` back, and a closed drawer\n * sliding away lets clicks through to the page it uncovers.\n */\n .v-drawer:not([open]) {\n display: none;\n translate: var(--drawer-offset);\n pointer-events: none;\n transition-timing-function: var(--vectis-ease-in);\n }\n\n @starting-style {\n .v-drawer[open] {\n translate: var(--drawer-offset);\n }\n }\n\n .v-drawer::backdrop {\n background: var(--vectis-color-backdrop);\n transition:\n opacity var(--vectis-duration-slow) var(--vectis-ease-default),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer:not([open])::backdrop {\n opacity: 0;\n }\n\n @starting-style {\n .v-drawer[open]::backdrop {\n opacity: 0;\n }\n }\n\n /* No ring around the whole drawer when opening focuses it: the controls inside show theirs. */\n .v-drawer:focus-visible {\n outline: none;\n }\n\n .v-drawer-scroll {\n /* The zero minimum lets the flex item shrink below its content, so that it scrolls. */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n container-type: scroll-state;\n }\n\n /* Negative margins make the sentinels occupy no space at all. */\n .v-drawer-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-drawer-edge--top {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-drawer-edge--bottom {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-drawer-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-drawer-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n .v-drawer-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* Pulled back by the cross's own padding, so its glyph lines up with the header's edge. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-drawer-body {\n /* Stretched when short, so the footer stays at the bottom of the drawer. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-drawer-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where scroll-state queries are missing, the sentinels simply stay transparent. */\n @container scroll-state(scrollable: top) {\n .v-drawer-edge--top {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-drawer-edge--bottom {\n background: var(--vectis-color-border);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-drawer,\n .v-drawer::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-drawer {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VDrawer.vue_vue_type_script_setup_true_lang.js","names":["$slots","_useModel","_openBlock","_createElementBlock","_Fragment","_renderSlot","_unref","_mergeProps","_createElementVNode","_createBlock","VTypography","_withCtx","_createTextVNode","_toDisplayString","_createCommentVNode","VIconButton","close","_createVNode","VIcon","closeIcon","VIcon_default","VTypography_default","VIconButton_default","_hoisted_1","_hoisted_2","_hoisted_3","_hoisted_4","_hoisted_5","_hoisted_6","_hoisted_7","VDrawer_vue_vue_type_script_setup_true_lang_default","\"open\"","\"openModifiers\"","__expose","props","__props","m","useMessages","resolvedCloseLabel","computed","open","useModel","titleId","useId","subtitleId","close$1","useModalDialog","triggerProps","show","dialogEl","openBlock","createElementBlock","Fragment","renderSlot","_ctx","unref","rendered","mergeProps","rootAttrs","cssSize","_cache","args","onClose","onCancel","onPointerdown","onBackdropClick","createElementVNode","createBlock","withCtx","createTextVNode","toDisplayString","createCommentVNode","createVNode","close"],"sources":["../../../src/components/VDrawer/VDrawer.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @core\n/**\n * A native modal `<dialog>` pinned to one edge of the viewport, its model bridged by\n * useModalDialog. It stays rendered through its exit, so it slides out as it slid in.\n */\n\nimport { computed, useId } from 'vue'\n\nimport type { DialogTriggerProps } from '../VDialog/VDialog.vue'\nimport VIcon from '../VIcon/VIcon.vue'\nimport { close as closeIcon } from '../VIcon/icons/close'\nimport VIconButton from '../VIconButton/VIconButton.vue'\nimport VTypography from '../VTypography/VTypography.vue'\nimport { useModalDialog } from '../../composables/useModalDialog'\nimport { useMessages } from '../../i18n/state'\nimport { cssSize } from '../../utils/css'\n\n/** The edge the drawer comes from. `start` and `end` follow the writing direction. */\nexport type DrawerSide = 'start' | 'end' | 'top' | 'bottom'\n\n/** The width of a drawer on a side, or the height of one at the top or bottom. */\nexport type DrawerSize = 'sm' | 'md' | 'lg'\n\ninterface DrawerProps {\n /**\n * The title of the drawer, which also names it for assistive technology. It is ignored when\n * the `#header` slot replaces the whole header.\n */\n title?: string\n /** A line under the title. */\n subtitle?: string\n /** The edge the drawer comes from. `start` and `end` follow the writing direction. */\n side?: DrawerSide\n /**\n * How far the drawer reaches into the page: its width on a side, its height at the top or\n * bottom. Each step reads a `--vectis-control-size-drawer-*` token.\n */\n size?: DrawerSize\n /**\n * A length replacing `size`: a number is read as pixels, a string as any CSS length. The\n * drawer always leaves a strip of the page uncovered, which a click closes it through.\n */\n extent?: number | string\n /**\n * Takes the close cross out of the header, leaving the reader with Escape, the backdrop and\n * whatever the footer offers.\n */\n hideClose?: boolean\n /** Stops a click outside the drawer from closing it. */\n persistentBackdrop?: boolean\n /**\n * Stops the Escape key from closing the drawer. Refusing Escape while the backdrop still\n * closes cannot be expressed natively, so both routes are then allowed.\n */\n persistentEscape?: boolean\n /** What the close cross does, in words. It falls back to the design system dictionary. */\n closeLabel?: string\n}\n\nconst props = withDefaults(defineProps<DrawerProps>(), {\n title: undefined,\n subtitle: undefined,\n side: 'end',\n size: 'md',\n extent: undefined,\n hideClose: false,\n persistentBackdrop: false,\n persistentEscape: false,\n closeLabel: undefined,\n})\n\nconst m = useMessages()\nconst resolvedCloseLabel = computed(() => props.closeLabel ?? m.value.common.close)\n\n/** Whether the drawer is showing. */\nconst open = defineModel<boolean>('open', { default: false })\n\ndefineSlots<{\n /** The body of the drawer. This is the part that scrolls when there is too much of it. */\n default(): unknown\n /** Replaces the title and subtitle block with content of your own. */\n header?(): unknown\n /** Extra controls in the header, placed before the close cross. */\n 'header-actions'?(): unknown\n /** The buttons at the foot of the drawer. */\n footer?(): unknown\n /**\n * The button that opens the drawer. Bind the `triggerProps` it receives onto it. It stays\n * rendered always, unlike the drawer itself.\n */\n trigger?(props: { triggerProps: DialogTriggerProps }): unknown\n}>()\n\ndefineOptions({ inheritAttrs: false })\n\nconst titleId = useId()\nconst subtitleId = useId()\n\nconst {\n dialogEl,\n rendered,\n rootAttrs,\n show,\n close,\n onClose,\n onCancel,\n onPointerdown,\n onBackdropClick,\n} = useModalDialog(open, {\n persistentBackdrop: () => props.persistentBackdrop,\n persistentEscape: () => props.persistentEscape,\n exitTransition: true,\n})\n\nconst triggerProps = computed<DialogTriggerProps>(() => ({\n onClick: show,\n 'aria-haspopup': 'dialog',\n}))\n\ndefineExpose({\n /**\n * Opens the drawer, exactly as setting `open` does. The opening lands on the next tick: the\n * promise returned settles once the drawer is showing.\n */\n show,\n /**\n * Closes the drawer. It closes unconditionally, `persistentEscape` and `persistentBackdrop`\n * governing only the two routes the reader can take.\n */\n close,\n /**\n * The `<dialog>` element. It is null while closed, once its exit has played: each opening\n * from there builds a fresh one.\n */\n el: dialogEl,\n})\n</script>\n\n<template>\n <slot name=\"trigger\" :trigger-props=\"triggerProps\" />\n <dialog\n v-if=\"rendered\"\n ref=\"dialogEl\"\n :aria-labelledby=\"title ? titleId : undefined\"\n :aria-describedby=\"subtitle ? subtitleId : undefined\"\n v-bind=\"rootAttrs\"\n class=\"v-drawer\"\n :data-side=\"side\"\n :data-size=\"size\"\n :style=\"{ '--drawer-extent': cssSize(extent) }\"\n @close=\"onClose\"\n @cancel=\"onCancel\"\n @pointerdown=\"onPointerdown\"\n @click=\"onBackdropClick\"\n >\n <!--\n As in VDialog, only the body scrolls, and the lines under the header and above the footer\n are drawn by sentinels stuck inside the scrolling area, which a scroll-state query reaches.\n -->\n <header class=\"v-drawer-header\">\n <slot name=\"header\">\n <div class=\"v-drawer-titles\">\n <VTypography\n v-if=\"title\"\n :id=\"titleId\"\n as=\"h2\"\n variant=\"heading-3\"\n class=\"v-drawer-title\"\n >\n {{ title }}\n </VTypography>\n <VTypography\n v-if=\"subtitle\"\n :id=\"subtitleId\"\n variant=\"subtitle\"\n tone=\"muted\"\n class=\"v-drawer-subtitle\"\n >\n {{ subtitle }}\n </VTypography>\n </div>\n </slot>\n <div v-if=\"!hideClose || $slots['header-actions']\" class=\"v-drawer-header-actions\">\n <slot name=\"header-actions\" />\n <VIconButton\n v-if=\"!hideClose\"\n class=\"v-drawer-close\"\n :label=\"resolvedCloseLabel\"\n variant=\"ghost\"\n tone=\"neutral\"\n size=\"sm\"\n @click=\"close\"\n >\n <VIcon :name=\"closeIcon\" />\n </VIconButton>\n </div>\n </header>\n <div class=\"v-drawer-scroll\">\n <span class=\"v-drawer-edge\" data-edge=\"top\" aria-hidden=\"true\" />\n <div class=\"v-drawer-body\">\n <slot />\n </div>\n <span class=\"v-drawer-edge\" data-edge=\"bottom\" aria-hidden=\"true\" />\n </div>\n <footer v-if=\"$slots.footer\" class=\"v-drawer-footer\">\n <slot name=\"footer\" />\n </footer>\n </dialog>\n</template>\n\n<style>\n@layer vectis.components {\n .v-drawer {\n /*\n * Declared on the element rather than as a `var()` fallback: a drawer opened from inside\n * another is its DOM descendant, and would otherwise inherit that drawer's inline extent.\n */\n --drawer-extent: var(--vectis-control-size-drawer-md);\n /* Where the drawer waits off screen, before it enters and after it leaves. */\n --drawer-offset: 100% 0;\n /*\n * Fixed explicitly rather than left to the modal's UA style: an engine without `overlay`\n * takes the drawer out of the top layer the moment it closes, and it must not fall back\n * into the page while it slides out.\n */\n position: fixed;\n inset: 0;\n margin: 0;\n inline-size: auto;\n block-size: auto;\n max-inline-size: none;\n max-block-size: none;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n padding: 0;\n border: none;\n background: var(--vectis-color-surface-overlay);\n color: var(--vectis-color-text);\n box-shadow: var(--vectis-shadow-xl);\n font-family: var(--vectis-text-family);\n /*\n * `display` and `overlay` switch at the END of the exit, which keeps the closed drawer\n * painted, and in the top layer, while it slides out.\n */\n transition:\n translate var(--vectis-duration-slow) var(--vectis-ease-out),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer[data-size='sm'] {\n --drawer-extent: var(--vectis-control-size-drawer-sm);\n }\n\n .v-drawer[data-size='lg'] {\n --drawer-extent: var(--vectis-control-size-drawer-lg);\n }\n\n /* The uncovered strip keeps the backdrop within reach, to see the page and to close. */\n .v-drawer[data-side='start'],\n .v-drawer[data-side='end'] {\n inline-size: var(--drawer-extent);\n max-inline-size: calc(100dvi - var(--vectis-space-12));\n }\n\n .v-drawer[data-side='top'],\n .v-drawer[data-side='bottom'] {\n block-size: var(--drawer-extent);\n max-block-size: calc(100dvb - var(--vectis-space-12));\n }\n\n /* The corners against the viewport stay square; the two facing the page are rounded. */\n .v-drawer[data-side='start'] {\n --drawer-offset: -100% 0;\n inset-inline-end: auto;\n border-start-end-radius: var(--vectis-radius-overlay);\n border-end-end-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='end'] {\n inset-inline-start: auto;\n border-start-start-radius: var(--vectis-radius-overlay);\n border-end-start-radius: var(--vectis-radius-overlay);\n }\n\n /* A translation is physical: in a right-to-left page the inline sides swap directions. */\n .v-drawer[data-side='start']:dir(rtl) {\n --drawer-offset: 100% 0;\n }\n\n .v-drawer[data-side='end']:dir(rtl) {\n --drawer-offset: -100% 0;\n }\n\n .v-drawer[data-side='top'] {\n --drawer-offset: 0 -100%;\n bottom: auto;\n border-bottom-left-radius: var(--vectis-radius-overlay);\n border-bottom-right-radius: var(--vectis-radius-overlay);\n }\n\n .v-drawer[data-side='bottom'] {\n --drawer-offset: 0 100%;\n top: auto;\n border-top-left-radius: var(--vectis-radius-overlay);\n border-top-right-radius: var(--vectis-radius-overlay);\n }\n\n /*\n * An indispensable guard: the display declared above beats the browser's own rule hiding a\n * closed dialog. Through the exit, the transition holds `display` back, and a closed drawer\n * sliding away lets clicks through to the page it uncovers.\n */\n .v-drawer:not([open]) {\n display: none;\n translate: var(--drawer-offset);\n pointer-events: none;\n transition-timing-function: var(--vectis-ease-in);\n }\n\n @starting-style {\n .v-drawer[open] {\n translate: var(--drawer-offset);\n }\n }\n\n .v-drawer::backdrop {\n background: var(--vectis-color-backdrop);\n transition:\n opacity var(--vectis-duration-slow) var(--vectis-ease-default),\n display var(--vectis-duration-slow) allow-discrete,\n overlay var(--vectis-duration-slow) allow-discrete;\n }\n\n .v-drawer:not([open])::backdrop {\n opacity: 0;\n }\n\n @starting-style {\n .v-drawer[open]::backdrop {\n opacity: 0;\n }\n }\n\n /* No ring around the whole drawer when opening focuses it: the controls inside show theirs. */\n .v-drawer:focus-visible {\n outline: none;\n }\n\n .v-drawer-scroll {\n /* The zero minimum lets the flex item shrink below its content, so that it scrolls. */\n flex: 1 1 auto;\n min-block-size: 0;\n overflow-y: auto;\n overscroll-behavior: contain;\n display: flex;\n flex-direction: column;\n container-type: scroll-state;\n }\n\n /* Negative margins make the sentinels occupy no space at all. */\n .v-drawer-edge {\n flex: none;\n block-size: 1px;\n position: sticky;\n background: transparent;\n }\n\n .v-drawer-edge[data-edge='top'] {\n inset-block-start: 0;\n margin-block-end: -1px;\n }\n\n .v-drawer-edge[data-edge='bottom'] {\n inset-block-end: 0;\n margin-block-start: -1px;\n }\n\n .v-drawer-header {\n flex: none;\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--vectis-space-4);\n padding: var(--vectis-space-6) var(--vectis-space-6) var(--vectis-space-3);\n }\n\n .v-drawer-titles {\n display: flex;\n flex-direction: column;\n min-inline-size: 0;\n }\n\n .v-drawer-header-actions {\n display: flex;\n align-items: center;\n gap: var(--vectis-space-1);\n /* Pulled back by the cross's own padding, so its glyph lines up with the header's edge. */\n margin-block-start: calc(-1 * var(--vectis-space-1));\n margin-inline-end: calc(-1 * var(--vectis-space-2));\n }\n\n .v-drawer-body {\n /* Stretched when short, so the footer stays at the bottom of the drawer. */\n flex: 1 0 auto;\n padding: var(--vectis-space-1) var(--vectis-space-6) var(--vectis-space-3);\n font-size: var(--vectis-text-body-md-size);\n line-height: var(--vectis-text-body-md-leading);\n }\n\n .v-drawer-footer {\n flex: none;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n flex-wrap: wrap;\n gap: var(--vectis-space-3);\n padding: var(--vectis-space-3) var(--vectis-space-6) var(--vectis-space-6);\n }\n\n /* Where scroll-state queries are missing, the sentinels simply stay transparent. */\n @container scroll-state(scrollable: top) {\n .v-drawer-edge[data-edge='top'] {\n background: var(--vectis-color-border);\n }\n }\n\n @container scroll-state(scrollable: bottom) {\n .v-drawer-edge[data-edge='bottom'] {\n background: var(--vectis-color-border);\n }\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-drawer,\n .v-drawer::backdrop {\n transition: none;\n }\n }\n\n /* An outline draws one without moving the layout by a pixel. */\n @media (forced-colors: active) {\n .v-drawer {\n outline: 1px solid CanvasText;\n }\n }\n}\n</style>\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4DA,IAAMkC,IAAQC,GAYRC,IAAIC,EAAY,GAChBC,IAAqBC,QAAeL,EAAM,cAAcE,EAAE,MAAM,OAAO,KAAK,GAG5EI,IAAOvC,EAAoBkC,GAAC,MAA0B,GAoBtDO,IAAUC,EAAM,GAChBC,IAAaD,EAAM,GAEnB,EACJ,aACA,aACA,cACA,SACA,OAAAE,GACA,YACA,aACA,kBACA,uBACEC,EAAeN,GAAM;GACvB,0BAA0BN,EAAM;GAChC,wBAAwBA,EAAM;GAC9B,gBAAgB;EAClB,CAAC,GAEKa,IAAeR,SAAoC;GACvD,SAASS;GACT,iBAAiB;EACnB,EAAE;SAEFf,EAAa;GAKX;GAKA,OAAAY;GAKA,IAAII;EACN,CAAC,cAIC5C,EAAAA,GAAAA,EAAAA,GAAAA,MAAAA,CAAAA,EAAqDiD,EAAA,QAAA,WAAA,EAA/B,cAAeP,EAAA,MAAY,CAAA,GAEzCzC,EAAAkD,CAAA,KADRtD,EAAA,GAAAC,EAmES,UAnETI,EAmES;;GAjEH,SAAA;GAAJ,KAAI0C;GACH,mBAAiBd,EAAA,QAAQ7B,EAAAoC,CAAA,IAAU,KAAA;GACnC,oBAAkBP,EAAA,WAAW7B,EAAAsC,CAAA,IAAa,KAAA;EACnC,GAAAtC,EAAAoD,CAAA,GAAS;GACjB,OAAM;GACL,aAAWvB,EAAA;GACX,aAAWA,EAAA;GACX,OAAK,EAAA,mBAAuB7B,EAAAqD,CAAA,CAAO,CAACxB,EAAA,MAAM,EAAA;GAC1C,SAAK,AAAAyB,EAAA,QAAE,GAAAC,MAAAvD,EAAAwD,CAAA,KAAAxD,EAAAwD,CAAA,CAAA,CAAA,GAAAD,CAAA;GACP,UAAM,AAAAD,EAAA,QAAE,GAAAC,MAAAvD,EAAAyD,CAAA,KAAAzD,EAAAyD,CAAA,CAAA,CAAA,GAAAF,CAAA;GACR,eAAW,AAAAD,EAAA,QAAE,GAAAC,MAAAvD,EAAA0D,CAAA,KAAA1D,EAAA0D,CAAA,CAAA,CAAA,GAAAH,CAAA;GACb,SAAK,AAAAD,EAAA,QAAE,GAAAC,MAAAvD,EAAA2D,CAAA,KAAA3D,EAAA2D,CAAA,CAAA,CAAA,GAAAJ,CAAA;;GAMRrD,EAqCS,UArCTgB,GAqCS,CApCPnB,EAqBOiD,EAAA,QAAA,UAAA,CAAA,SAAA,CApBL9C,EAmBM,OAnBNiB,GAmBM,CAjBIU,EAAA,SADRjC,EAAA,GAAAO,EAQcC,GAAA;;IANX,IAAIJ,EAAAoC,CAAA;IACL,IAAG;IACH,SAAQ;IACR,OAAM;;IAEN,SAAA/B,QAAW,CAARC,EAAAC,EAAAsB,EAAA,KAAK,GAAA,CAAA,CAAA,CAAA;;GAGF,GAAA,GAAA,CAAA,IAAA,CAAA,KAAAoC,EAAA,IAAA,EAAA,GAAApC,EAAA,YADRjC,EAAA,GAAAO,EAQcC,GAAA;;IANX,IAAIJ,EAAAsC,CAAA;IACL,SAAQ;IACR,MAAK;IACL,OAAM;;IAEN,SAAAjC,QAAc,CAAXC,EAAAC,EAAAsB,EAAA,QAAQ,GAAA,CAAA,CAAA,CAAA;;GAIL,GAAA,GAAA,CAAA,IAAA,CAAA,KAAAoC,EAAA,IAAA,EAAA,CAAA,CAAA,CAAA,CAAA,GAAA,CAAApC,EAAA,aAAanC,EAAAA,OAAM,qBAA/BE,EAAA,GAAAC,EAaM,OAbNuB,GAaM,CAZJrB,EAA8BiD,EAAA,QAAA,gBAAA,GAErBnB,EAAA,yBADTjC,EAAA,GAAAO,EAUcM,GAAA;;IARZ,OAAM;IACL,OAAOuB,EAAA;IACR,SAAQ;IACR,MAAK;IACL,MAAK;IACJ,SAAOhC,EAAAU,CAAA;;IAER,SAAAL,QAA2B,CAA3BM,EAA2BC,GAAA,EAAnB,MAAMZ,EAAAa,CAAA,EAAS,GAAA,MAAA,GAAA,CAAA,MAAA,CAAA,CAAA,CAAA;;;GAI7BX,EAMM,OANNmB,GAMM;IALJ,AAAAiC,EAAA,OAAApD,EAAiE,QAAA;KAA3D,OAAM;KAAgB,aAAU;KAAM,eAAY;;IACxDA,EAEM,OAFNoB,GAEM,CADJvB,EAAQiD,EAAA,QAAA,SAAA,CAAA,CAAA;IAEV,AAAAM,EAAA,OAAApD,EAAoE,QAAA;KAA9D,OAAM;KAAgB,aAAU;KAAS,eAAY;;;GAE/CR,EAAAA,OAAO,UAArBE,EAAA,GAAAC,EAES,UAFT0B,GAES,CADPxB,EAAsBiD,EAAA,QAAA,QAAA,CAAA,CAAA,KAAAiB,EAAA,IAAA,EAAA"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
@layer vectis.reset, vectis.tokens, vectis.components, vectis.utilities;
|
|
2
|
-
@layer vectis.components{.v-meter{--meter-thickness:var(--vectis-control-size-
|
|
2
|
+
@layer vectis.components{.v-meter{--meter-thickness:var(--vectis-control-size-bar-thickness-md);--meter-fill:var(--tone-bg-solid);--meter-track:var(--tone-bg-soft);--meter-text-fallback:var(--tone-text-solid);gap:var(--vectis-space-1);min-inline-size:0;font-family:var(--vectis-text-family);flex-direction:column;display:flex}.v-meter[data-size=xs]{--meter-thickness:var(--vectis-control-size-bar-thickness-xs)}.v-meter[data-size=sm]{--meter-thickness:var(--vectis-control-size-bar-thickness-sm)}.v-meter[data-size=lg]{--meter-thickness:var(--vectis-control-size-bar-thickness-lg)}.v-meter[data-size=xl]{--meter-thickness:var(--vectis-control-size-bar-thickness-xl)}.v-meter-header{align-items:baseline;column-gap:var(--vectis-space-2);flex-wrap:wrap;display:flex}.v-meter-label{color:var(--vectis-color-text);font-size:var(--vectis-text-label-size);font-weight:var(--vectis-text-label-weight);line-height:var(--vectis-text-label-leading)}.v-meter-value{color:var(--vectis-color-text-muted);font-size:var(--vectis-text-body-md-size);font-weight:var(--vectis-text-body-md-weight);line-height:var(--vectis-text-body-md-leading);font-variant-numeric:tabular-nums;margin-inline-start:auto}.v-meter[data-orientation=vertical]{block-size:var(--vectis-control-size-bar-length);align-items:flex-start}.v-meter-track{--meter-edge:calc((100% - (var(--meter-segments) - 1) * var(--vectis-control-size-meter-segment-gap)) * var(--meter-fraction) + var(--meter-gaps-filled) * var(--vectis-control-size-meter-segment-gap));gap:var(--vectis-control-size-meter-segment-gap);block-size:var(--meter-thickness);display:flex;position:relative}.v-meter-segment{border-radius:var(--vectis-radius-pill);background:var(--meter-track);flex:1 1 0;position:relative}.v-meter-fill{inline-size:calc(100% * var(--meter-segment-fill));border-radius:inherit;background:var(--meter-fill);transition:inline-size var(--vectis-duration-base) var(--vectis-ease-default), background-color var(--vectis-duration-base) var(--vectis-ease-default);position:absolute;inset-block:0;inset-inline-start:0}.v-meter-text{--meter-clip-start:var(--meter-edge);--meter-clip-end:-100vmax;padding-inline:var(--vectis-space-2);color:var(--vectis-color-text);font-size:var(--vectis-text-caption-size);font-weight:var(--vectis-text-control-weight);line-height:var(--vectis-text-control-leading);font-variant-numeric:tabular-nums;clip-path:inset(-100vmax var(--meter-clip-end) -100vmax var(--meter-clip-start));transition:clip-path var(--vectis-duration-base) var(--vectis-ease-default);white-space:nowrap;pointer-events:none;justify-content:center;align-items:center;display:flex;position:absolute;inset:0}.v-meter[data-value-position=start] .v-meter-text{justify-content:start}.v-meter[data-value-position=end] .v-meter-text{justify-content:end}.v-meter-text[data-on-fill]{--meter-clip-start:-100vmax;--meter-clip-end:calc(100% - var(--meter-edge));color:var(--meter-text-fallback)}@supports (color:contrast-color(red)){.v-meter-text[data-on-fill]{color:contrast-color(var(--meter-fill))}}.v-meter-track:dir(rtl) .v-meter-text{clip-path:inset(-100vmax var(--meter-clip-start) -100vmax var(--meter-clip-end))}.v-meter[data-orientation=vertical] .v-meter-track{writing-mode:vertical-lr;flex-direction:row-reverse;flex:1 1 0}.v-meter[data-orientation=vertical] .v-meter-fill{inset-inline:auto 0}.v-meter[data-orientation=vertical] .v-meter-track:dir(rtl){flex-direction:row}.v-meter[data-orientation=vertical] .v-meter-track:dir(rtl) .v-meter-fill{inset-inline:0 auto}.v-meter[data-orientation=vertical] .v-meter-text{padding-inline:0;padding-block:var(--vectis-space-2);writing-mode:horizontal-tb;flex-direction:column-reverse}.v-meter[data-orientation=vertical] .v-meter-text,.v-meter[data-orientation=vertical] .v-meter-track:dir(rtl) .v-meter-text{clip-path:inset(var(--meter-clip-end) -100vmax var(--meter-clip-start) -100vmax)}@media (prefers-reduced-motion:reduce){.v-meter-fill,.v-meter-text{transition:none}}@media (forced-colors:active){.v-meter-segment{forced-color-adjust:none;box-shadow:inset 0 0 0 var(--vectis-control-border-width) CanvasText;background:canvas}.v-meter-fill{background:highlight}.v-meter-text{forced-color-adjust:none;color:canvastext}.v-meter-text[data-on-fill]{color:highlighttext}}}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
/** The colour of the fill, which replaces the one chosen from `low`, `high` and `optimum`. */
|
|
2
2
|
export type MeterTone = 'accent' | 'neutral' | 'success' | 'warning' | 'danger';
|
|
3
|
-
/** The thickness of the bar, 4, 8 or
|
|
4
|
-
export type MeterSize = 'sm' | 'md' | 'lg';
|
|
3
|
+
/** The thickness of the bar: 2, 4, 8, 12 or 20 pixels. */
|
|
4
|
+
export type MeterSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
5
|
+
/** Where the value is written: after the label, or inside the bar at its start, centre or end. */
|
|
6
|
+
export type MeterValuePosition = 'header' | 'start' | 'center' | 'end';
|
|
7
|
+
/** Which way the bar fills. */
|
|
8
|
+
export type MeterOrientation = 'horizontal' | 'vertical';
|
|
5
9
|
interface MeterProps {
|
|
6
10
|
/** The measurement. Anything outside the range is brought back into it. */
|
|
7
11
|
value?: number;
|
|
@@ -30,8 +34,8 @@ interface MeterProps {
|
|
|
30
34
|
/** Hides the label visually. It still names the bar for screen readers. */
|
|
31
35
|
hideLabel?: boolean;
|
|
32
36
|
/**
|
|
33
|
-
* The value as written
|
|
34
|
-
*
|
|
37
|
+
* The value as written and read by screen readers, such as "7.2 GB of 10 GB" or "Strong". It
|
|
38
|
+
* replaces the formatted value.
|
|
35
39
|
*/
|
|
36
40
|
valueText?: string;
|
|
37
41
|
/**
|
|
@@ -39,8 +43,13 @@ interface MeterProps {
|
|
|
39
43
|
* value is written as a percentage of the range.
|
|
40
44
|
*/
|
|
41
45
|
formatOptions?: Intl.NumberFormatOptions;
|
|
42
|
-
/**
|
|
43
|
-
|
|
46
|
+
/** Writes the value. Screen readers read it from the bar either way. */
|
|
47
|
+
showValue?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Where that text is written: at the end of the label's line, or inside the bar. On a vertical
|
|
50
|
+
* bar the start is the minimum end, hence the bottom.
|
|
51
|
+
*/
|
|
52
|
+
valuePosition?: MeterValuePosition;
|
|
44
53
|
/**
|
|
45
54
|
* The colour of the fill. Without it, the region of the value picks one when `low`, `high` or
|
|
46
55
|
* `optimum` is set: success, warning or danger; otherwise the accent.
|
|
@@ -53,8 +62,13 @@ interface MeterProps {
|
|
|
53
62
|
color?: string;
|
|
54
63
|
/** Splits the bar into this many equal parts, for a strength or a battery level. */
|
|
55
64
|
segments?: number;
|
|
56
|
-
/** The thickness of the bar
|
|
65
|
+
/** The thickness of the bar. Only `xl` is thick enough to hold the value written inside it. */
|
|
57
66
|
size?: MeterSize;
|
|
67
|
+
/**
|
|
68
|
+
* Turns the bar upright, filling from the bottom up. Its length is then a height set on the
|
|
69
|
+
* component.
|
|
70
|
+
*/
|
|
71
|
+
orientation?: MeterOrientation;
|
|
58
72
|
}
|
|
59
73
|
declare const __VLS_export: import('vue').DefineComponent<MeterProps, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {}, string, import('vue').PublicProps, Readonly<MeterProps> & Readonly<{}>, {
|
|
60
74
|
label: string;
|
|
@@ -62,6 +76,7 @@ declare const __VLS_export: import('vue').DefineComponent<MeterProps, {}, {}, {}
|
|
|
62
76
|
size: MeterSize;
|
|
63
77
|
tone: MeterTone;
|
|
64
78
|
color: string;
|
|
79
|
+
orientation: MeterOrientation;
|
|
65
80
|
hideLabel: boolean;
|
|
66
81
|
min: number;
|
|
67
82
|
max: number;
|
|
@@ -71,7 +86,8 @@ declare const __VLS_export: import('vue').DefineComponent<MeterProps, {}, {}, {}
|
|
|
71
86
|
low: number;
|
|
72
87
|
high: number;
|
|
73
88
|
optimum: number;
|
|
74
|
-
|
|
89
|
+
showValue: boolean;
|
|
90
|
+
valuePosition: MeterValuePosition;
|
|
75
91
|
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
76
92
|
declare const _default: typeof __VLS_export;
|
|
77
93
|
export default _default;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VMeter.js","names":["_sfc_main","VMeter_vue_vue_type_script_setup_true_lang_default","VMeter_default"],"sources":["../../../src/components/VMeter/VMeter.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y\n/**\n * A measurement within a known range, drawn in CSS under the `meter` role. The native `<meter>`\n * is styled only through vendor pseudo-elements, which need one rule set per engine, does not\n * transition its fill in Firefox and cannot be split into segments; the role carries the same\n * semantics. As with the native element, `low`, `high` and `optimum` place the value in a\n * region, which picks the tone.\n */\nimport { computed, useId } from 'vue'\n\nimport { useAriaLabel } from '../../composables/useAriaLabel'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { customColorStyle } from '../../utils/css'\nimport { clamp } from '../../utils/number'\n\n/** The colour of the fill, which replaces the one chosen from `low`, `high` and `optimum`. */\nexport type MeterTone = 'accent' | 'neutral' | 'success' | 'warning' | 'danger'\n\n/** The thickness of the bar, 4, 8 or 12 pixels, and the size of the text above it. */\nexport type MeterSize = 'sm' | 'md' | 'lg'\n\ninterface MeterProps {\n /** The measurement. Anything outside the range is brought back into it. */\n value?: number\n /** The lower bound of the range. */\n min?: number\n /** The upper bound of the range. A bound below `min` is raised to it. */\n max?: number\n /**\n * The upper end of the low part of the range. With `high` and `optimum`, it decides whether\n * the value is good, average or poor, which colours the bar.\n */\n low?: number\n /** The lower end of the high part of the range. */\n high?: number\n /**\n * The best value. In the low part, low values are good; in the high part, high values are;\n * between `low` and `high`, the middle is good and both ends are average.\n */\n optimum?: number\n /**\n * What is measured, shown above the bar and naming it for screen readers. Without it, the bar\n * is named from the design system dictionary; an `aria-label` or `aria-labelledby` of your own\n * takes precedence.\n */\n label?: string\n /** Hides the label visually. It still names the bar for screen readers. */\n hideLabel?: boolean\n /**\n * The value as written above the bar and read by screen readers, such as \"7.2 GB of 10 GB\" or\n * \"Strong\". It replaces the formatted value.\n */\n valueText?: string\n /**\n * Formats the value for the current locale, such as a unit or a currency. Without it, the\n * value is written as a percentage of the range.\n */\n formatOptions?: Intl.NumberFormatOptions\n /** Hides the value visually. Screen readers still read it from the bar. */\n hideValue?: boolean\n /**\n * The colour of the fill. Without it, the region of the value picks one when `low`, `high` or\n * `optimum` is set: success, warning or danger; otherwise the accent.\n */\n tone?: MeterTone\n /**\n * A colour of your own (hex, CSS name or `oklch()`), which replaces the tone. The track is\n * derived from it against the theme.\n */\n color?: string\n /** Splits the bar into this many equal parts, for a strength or a battery level. */\n segments?: number\n /** The thickness of the bar and the size of the text above it. */\n size?: MeterSize\n}\n\n// The root only lays out the text and the bar: the consumer's ARIA and other attributes belong\n// on the element carrying the role.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MeterProps>(), {\n value: 0,\n min: 0,\n max: 100,\n low: undefined,\n high: undefined,\n optimum: undefined,\n label: undefined,\n hideLabel: false,\n valueText: undefined,\n formatOptions: undefined,\n hideValue: false,\n // Deliberately undefined: the region of the value then picks the tone.\n tone: undefined,\n color: undefined,\n segments: 1,\n size: 'md',\n})\n\nconst { attrs, rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\nconst m = useMessages()\nconst locale = useLocale()\nconst labelId = useId()\n\n/* The normalization of the HTML `<meter>`: the bounds first, then each point brought into\n them, `high` never below `low`. A value that is not a number reads as the minimum. */\nconst range = computed(() => {\n const min = Number.isFinite(props.min) ? props.min : 0\n const max = Number.isFinite(props.max) ? Math.max(props.max, min) : Math.max(100, min)\n const fit = (point: number | undefined, from: number, fallback: number) =>\n point === undefined || Number.isNaN(point) ? fallback : clamp(point, from, max)\n const low = fit(props.low, min, min)\n return {\n min,\n max,\n value: fit(props.value, min, min),\n low,\n high: fit(props.high, low, max),\n optimum: fit(props.optimum, min, (min + max) / 2),\n }\n})\n\nconst fraction = computed(() => {\n const { min, max, value } = range.value\n return max > min ? (value - min) / (max - min) : 0\n})\n\n// @core\n// The regions of the HTML `<meter>`, with the boundaries Chromium draws: the part holding the\n// optimum is good, the part next to it average, and the far part, when the optimum sits beyond\n// `low` or `high`, poor.\nconst level = computed<'optimum' | 'suboptimal' | 'poor'>(() => {\n const { value, low, high, optimum } = range.value\n if (optimum >= low && optimum <= high)\n return value >= low && value <= high ? 'optimum' : 'suboptimal'\n if (optimum < low) return value < low ? 'optimum' : value <= high ? 'suboptimal' : 'poor'\n return value > high ? 'optimum' : value >= low ? 'suboptimal' : 'poor'\n})\n\nconst LEVEL_TONES = { optimum: 'success', suboptimal: 'warning', poor: 'danger' } as const\n\nconst resolvedTone = computed<MeterTone>(() => {\n if (props.tone) return props.tone\n const graded = props.low !== undefined || props.high !== undefined || props.optimum !== undefined\n return graded ? LEVEL_TONES[level.value] : 'accent'\n})\n\nconst text = computed(() => {\n if (props.valueText !== undefined) return props.valueText\n if (props.formatOptions)\n return new Intl.NumberFormat(locale.value, props.formatOptions).format(range.value.value)\n return m.value.progress.percent(Math.round(fraction.value * 100))\n})\n\nconst segmentCount = computed(() =>\n Number.isFinite(props.segments) ? Math.max(1, Math.floor(props.segments)) : 1,\n)\n\n/** How much of segment `index` is filled, from 0 to 1. */\nconst segmentFill = (index: number) => clamp(fraction.value * segmentCount.value - index, 0, 1)\n\n// @a11y\n// A visible label names the bar by reference, unless the consumer named it themselves; without\n// one, the dictionary does.\nconst ariaLabel = useAriaLabel(() => (props.label ? undefined : m.value.meter.label))\nconst labelledBy = computed(() =>\n props.label && attrs['aria-label'] == null && attrs['aria-labelledby'] == null\n ? labelId\n : undefined,\n)\n</script>\n\n<template>\n <div\n class=\"v-meter v-tone\"\n :class=\"rootClass\"\n :style=\"[customColorStyle(color), rootStyle]\"\n :data-tone=\"resolvedTone\"\n :data-custom=\"color !== undefined ? '' : undefined\"\n :data-level=\"level\"\n :data-size=\"size\"\n >\n <div v-if=\"(label && !hideLabel) || !hideValue\" class=\"v-meter-header\">\n <span v-if=\"label && !hideLabel\" :id=\"labelId\" class=\"v-meter-label\">{{ label }}</span>\n <!-- Read from the bar's `aria-valuetext`, so hidden here to avoid reading it twice. -->\n <span v-if=\"!hideValue\" class=\"v-meter-value\" aria-hidden=\"true\">{{ text }}</span>\n </div>\n <span v-if=\"label && hideLabel\" :id=\"labelId\" class=\"v-visually-hidden\">{{ label }}</span>\n <div\n class=\"v-meter-track\"\n role=\"meter\"\n :aria-valuenow=\"range.value\"\n :aria-valuemin=\"range.min\"\n :aria-valuemax=\"range.max\"\n :aria-valuetext=\"text\"\n :aria-labelledby=\"labelledBy\"\n :aria-label=\"ariaLabel\"\n v-bind=\"forwardedAttrs\"\n >\n <span\n v-for=\"segment in segmentCount\"\n :key=\"segment\"\n class=\"v-meter-segment\"\n :style=\"{ '--meter-segment-fill': String(segmentFill(segment - 1)) }\"\n >\n <span class=\"v-meter-fill\" />\n </span>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-meter {\n --meter-thickness: var(--vectis-control-size-meter-thickness-md);\n /*\n * Fill and track come from the shared tone table. The track takes the soft border rather than\n * the soft background: the empty part must stay visible, since the reading is a proportion.\n */\n --meter-fill: var(--tone-bg-solid);\n --meter-track: var(--tone-border-soft);\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n min-inline-size: 0;\n font-family: var(--vectis-text-family);\n }\n\n /* The custom-colour row of the tone table defines no soft border; the tone's would show. */\n .v-meter[data-custom] {\n --meter-track: color-mix(in oklab, var(--custom-color), var(--vectis-color-surface) 70%);\n }\n\n .v-meter[data-size='sm'] {\n --meter-thickness: var(--vectis-control-size-meter-thickness-sm);\n }\n\n .v-meter[data-size='lg'] {\n --meter-thickness: var(--vectis-control-size-meter-thickness-lg);\n }\n\n .v-meter-header {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: var(--vectis-space-2);\n }\n\n .v-meter-label {\n color: var(--vectis-color-text);\n font-size: var(--vectis-text-label-size);\n font-weight: var(--vectis-text-label-weight);\n line-height: var(--vectis-text-label-leading);\n }\n\n /* Pushed to the end even without a label before it. */\n .v-meter-value {\n margin-inline-start: auto;\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-body-md-size);\n font-weight: var(--vectis-text-body-md-weight);\n line-height: var(--vectis-text-body-md-leading);\n font-variant-numeric: tabular-nums;\n }\n\n .v-meter[data-size='sm'] .v-meter-label,\n .v-meter[data-size='sm'] .v-meter-value {\n font-size: var(--vectis-text-caption-size);\n font-weight: var(--vectis-text-caption-weight);\n line-height: var(--vectis-text-caption-leading);\n }\n\n .v-meter-track {\n display: flex;\n gap: var(--vectis-control-size-meter-segment-gap);\n block-size: var(--meter-thickness);\n }\n\n .v-meter-segment {\n position: relative;\n flex: 1 1 0;\n border-radius: var(--vectis-radius-pill);\n background: var(--meter-track);\n }\n\n .v-meter-fill {\n position: absolute;\n inset-block: 0;\n inset-inline-start: 0;\n inline-size: calc(100% * var(--meter-segment-fill));\n border-radius: inherit;\n background: var(--meter-fill);\n /* A logical length rather than a scale, which would have to be undone in right to left. */\n transition:\n inline-size var(--vectis-duration-base) var(--vectis-ease-default),\n background-color var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-meter-fill {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .v-meter-segment {\n forced-color-adjust: none;\n background: Canvas;\n box-shadow: inset 0 0 0 var(--vectis-control-border-width) CanvasText;\n }\n\n .v-meter-fill {\n background: Highlight;\n }\n }\n}\n</style>\n"],"mappings":""}
|
|
1
|
+
{"version":3,"file":"VMeter.js","names":["_sfc_main","VMeter_vue_vue_type_script_setup_true_lang_default","VMeter_default"],"sources":["../../../src/components/VMeter/VMeter.vue"],"sourcesContent":["<script setup lang=\"ts\">\n// @a11y\n/**\n * A measurement within a known range, drawn in CSS under the `meter` role. The native `<meter>`\n * is styled only through vendor pseudo-elements, which need one rule set per engine, does not\n * transition its fill in Firefox and cannot be split into segments; the role carries the same\n * semantics. As with the native element, `low`, `high` and `optimum` place the value in a\n * region, which picks the tone.\n */\nimport { computed, useId } from 'vue'\n\nimport { useAriaLabel, useLabelledBy } from '../../composables/useAriaLabel'\nimport { useRootAttrs } from '../../composables/useRootAttrs'\nimport { useLocale, useMessages } from '../../i18n/state'\nimport { customColorStyle } from '../../utils/css'\nimport { clamp } from '../../utils/number'\n\n/** The colour of the fill, which replaces the one chosen from `low`, `high` and `optimum`. */\nexport type MeterTone = 'accent' | 'neutral' | 'success' | 'warning' | 'danger'\n\n/** The thickness of the bar: 2, 4, 8, 12 or 20 pixels. */\nexport type MeterSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl'\n\n/** Where the value is written: after the label, or inside the bar at its start, centre or end. */\nexport type MeterValuePosition = 'header' | 'start' | 'center' | 'end'\n\n/** Which way the bar fills. */\nexport type MeterOrientation = 'horizontal' | 'vertical'\n\ninterface MeterProps {\n /** The measurement. Anything outside the range is brought back into it. */\n value?: number\n /** The lower bound of the range. */\n min?: number\n /** The upper bound of the range. A bound below `min` is raised to it. */\n max?: number\n /**\n * The upper end of the low part of the range. With `high` and `optimum`, it decides whether\n * the value is good, average or poor, which colours the bar.\n */\n low?: number\n /** The lower end of the high part of the range. */\n high?: number\n /**\n * The best value. In the low part, low values are good; in the high part, high values are;\n * between `low` and `high`, the middle is good and both ends are average.\n */\n optimum?: number\n /**\n * What is measured, shown above the bar and naming it for screen readers. Without it, the bar\n * is named from the design system dictionary; an `aria-label` or `aria-labelledby` of your own\n * takes precedence.\n */\n label?: string\n /** Hides the label visually. It still names the bar for screen readers. */\n hideLabel?: boolean\n /**\n * The value as written and read by screen readers, such as \"7.2 GB of 10 GB\" or \"Strong\". It\n * replaces the formatted value.\n */\n valueText?: string\n /**\n * Formats the value for the current locale, such as a unit or a currency. Without it, the\n * value is written as a percentage of the range.\n */\n formatOptions?: Intl.NumberFormatOptions\n /** Writes the value. Screen readers read it from the bar either way. */\n showValue?: boolean\n /**\n * Where that text is written: at the end of the label's line, or inside the bar. On a vertical\n * bar the start is the minimum end, hence the bottom.\n */\n valuePosition?: MeterValuePosition\n /**\n * The colour of the fill. Without it, the region of the value picks one when `low`, `high` or\n * `optimum` is set: success, warning or danger; otherwise the accent.\n */\n tone?: MeterTone\n /**\n * A colour of your own (hex, CSS name or `oklch()`), which replaces the tone. The track is\n * derived from it against the theme.\n */\n color?: string\n /** Splits the bar into this many equal parts, for a strength or a battery level. */\n segments?: number\n /** The thickness of the bar. Only `xl` is thick enough to hold the value written inside it. */\n size?: MeterSize\n /**\n * Turns the bar upright, filling from the bottom up. Its length is then a height set on the\n * component.\n */\n orientation?: MeterOrientation\n}\n\n// The root only lays out the text and the bar: the consumer's ARIA and other attributes belong\n// on the element carrying the role.\ndefineOptions({ inheritAttrs: false })\n\nconst props = withDefaults(defineProps<MeterProps>(), {\n value: 0,\n min: 0,\n max: 100,\n low: undefined,\n high: undefined,\n optimum: undefined,\n label: undefined,\n hideLabel: false,\n valueText: undefined,\n formatOptions: undefined,\n showValue: false,\n valuePosition: 'header',\n // Deliberately undefined: the region of the value then picks the tone.\n tone: undefined,\n color: undefined,\n segments: 1,\n size: 'md',\n orientation: 'horizontal',\n})\n\nconst { rootClass, rootStyle, forwardedAttrs } = useRootAttrs()\nconst m = useMessages()\nconst locale = useLocale()\nconst labelId = useId()\n\n/* The normalization of the HTML `<meter>`: the bounds first, then each point brought into\n them, `high` never below `low`. A value that is not a number reads as the minimum. */\nconst range = computed(() => {\n const min = Number.isFinite(props.min) ? props.min : 0\n const max = Number.isFinite(props.max) ? Math.max(props.max, min) : Math.max(100, min)\n const fit = (point: number | undefined, from: number, fallback: number) =>\n point === undefined || Number.isNaN(point) ? fallback : clamp(point, from, max)\n const low = fit(props.low, min, min)\n return {\n min,\n max,\n value: fit(props.value, min, min),\n low,\n high: fit(props.high, low, max),\n optimum: fit(props.optimum, min, (min + max) / 2),\n }\n})\n\nconst fraction = computed(() => {\n const { min, max, value } = range.value\n return max > min ? (value - min) / (max - min) : 0\n})\n\n// @core\n// The regions of the HTML `<meter>`, with the boundaries Chromium draws: the part holding the\n// optimum is good, the part next to it average, and the far part, when the optimum sits beyond\n// `low` or `high`, poor.\nconst level = computed<'optimum' | 'suboptimal' | 'poor'>(() => {\n const { value, low, high, optimum } = range.value\n if (optimum >= low && optimum <= high)\n return value >= low && value <= high ? 'optimum' : 'suboptimal'\n if (optimum < low) return value < low ? 'optimum' : value <= high ? 'suboptimal' : 'poor'\n return value > high ? 'optimum' : value >= low ? 'suboptimal' : 'poor'\n})\n\nconst LEVEL_TONES = { optimum: 'success', suboptimal: 'warning', poor: 'danger' } as const\n\nconst resolvedTone = computed<MeterTone>(() => {\n if (props.tone) return props.tone\n const graded = props.low !== undefined || props.high !== undefined || props.optimum !== undefined\n return graded ? LEVEL_TONES[level.value] : 'accent'\n})\n\nconst text = computed(() => {\n if (props.valueText !== undefined) return props.valueText\n if (props.formatOptions)\n return new Intl.NumberFormat(locale.value, props.formatOptions).format(range.value.value)\n return m.value.progress.percent(Math.round(fraction.value * 100))\n})\n\nconst segmentCount = computed(() =>\n Number.isFinite(props.segments) ? Math.max(1, Math.floor(props.segments)) : 1,\n)\n\n/** How much of segment `index` is filled, from 0 to 1. */\nconst segmentFill = (index: number) => clamp(fraction.value * segmentCount.value - index, 0, 1)\n\nconst valueInHeader = computed(() => props.showValue && props.valuePosition === 'header')\nconst valueInBar = computed(() => props.showValue && props.valuePosition !== 'header')\n\n// @a11y\n// A visible label names the bar by reference, unless the consumer named it themselves; without\n// one, the dictionary does.\nconst ariaLabel = useAriaLabel(() => (props.label ? undefined : m.value.meter.label))\nconst labelledBy = useLabelledBy(labelId, () => props.label)\n</script>\n\n<template>\n <div\n class=\"v-meter v-tone\"\n :class=\"rootClass\"\n :style=\"[customColorStyle(color), rootStyle]\"\n :data-tone=\"resolvedTone\"\n :data-custom=\"color !== undefined ? '' : undefined\"\n :data-level=\"level\"\n :data-size=\"size\"\n :data-value-position=\"valuePosition\"\n :data-orientation=\"orientation\"\n >\n <div v-if=\"(label && !hideLabel) || valueInHeader\" class=\"v-meter-header\">\n <span v-if=\"label && !hideLabel\" :id=\"labelId\" class=\"v-meter-label\">{{ label }}</span>\n <!-- Read from the bar's `aria-valuetext`, so hidden here to avoid reading it twice. -->\n <span v-if=\"valueInHeader\" class=\"v-meter-value\" aria-hidden=\"true\">{{ text }}</span>\n </div>\n <span v-if=\"label && hideLabel\" :id=\"labelId\" class=\"v-visually-hidden\">{{ label }}</span>\n <div\n class=\"v-meter-track\"\n role=\"meter\"\n :aria-valuenow=\"range.value\"\n :aria-valuemin=\"range.min\"\n :aria-valuemax=\"range.max\"\n :aria-valuetext=\"text\"\n :aria-labelledby=\"labelledBy\"\n :aria-label=\"ariaLabel\"\n v-bind=\"forwardedAttrs\"\n :style=\"{\n '--meter-fraction': String(fraction),\n '--meter-segments': String(segmentCount),\n '--meter-gaps-filled': String(Math.floor(fraction * segmentCount)),\n }\"\n >\n <span\n v-for=\"segment in segmentCount\"\n :key=\"segment\"\n class=\"v-meter-segment\"\n :style=\"{ '--meter-segment-fill': String(segmentFill(segment - 1)) }\"\n >\n <span class=\"v-meter-fill\" />\n </span>\n <!--\n The same text twice, as in VProgressLinear: each copy is cut at the fill's edge, the\n second coloured to contrast with the fill. Both are hidden, `aria-valuetext` carrying\n the text.\n -->\n <template v-if=\"valueInBar\">\n <span class=\"v-meter-text\" aria-hidden=\"true\">{{ text }}</span>\n <span class=\"v-meter-text\" data-on-fill aria-hidden=\"true\">{{ text }}</span>\n </template>\n </div>\n </div>\n</template>\n\n<style>\n@layer vectis.components {\n .v-meter {\n --meter-thickness: var(--vectis-control-size-bar-thickness-md);\n /* Fill, track and the text over the fill come from the shared tone table, as in\n VProgressLinear. */\n --meter-fill: var(--tone-bg-solid);\n --meter-track: var(--tone-bg-soft);\n --meter-text-fallback: var(--tone-text-solid);\n display: flex;\n flex-direction: column;\n gap: var(--vectis-space-1);\n min-inline-size: 0;\n font-family: var(--vectis-text-family);\n }\n\n .v-meter[data-size='xs'] {\n --meter-thickness: var(--vectis-control-size-bar-thickness-xs);\n }\n\n .v-meter[data-size='sm'] {\n --meter-thickness: var(--vectis-control-size-bar-thickness-sm);\n }\n\n .v-meter[data-size='lg'] {\n --meter-thickness: var(--vectis-control-size-bar-thickness-lg);\n }\n\n .v-meter[data-size='xl'] {\n --meter-thickness: var(--vectis-control-size-bar-thickness-xl);\n }\n\n .v-meter-header {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n column-gap: var(--vectis-space-2);\n }\n\n .v-meter-label {\n color: var(--vectis-color-text);\n font-size: var(--vectis-text-label-size);\n font-weight: var(--vectis-text-label-weight);\n line-height: var(--vectis-text-label-leading);\n }\n\n /* Pushed to the end even without a label before it. */\n .v-meter-value {\n margin-inline-start: auto;\n color: var(--vectis-color-text-muted);\n font-size: var(--vectis-text-body-md-size);\n font-weight: var(--vectis-text-body-md-weight);\n line-height: var(--vectis-text-body-md-leading);\n font-variant-numeric: tabular-nums;\n }\n\n /*\n * Upright, the root carries the length, so a consumer's height on it sets the bar's: the track\n * takes whatever the header leaves.\n */\n .v-meter[data-orientation='vertical'] {\n align-items: flex-start;\n block-size: var(--vectis-control-size-bar-length);\n }\n\n .v-meter-track {\n /*\n * Where the fill ends along the whole track, gaps included: the filled fraction of the\n * segments' total length, plus every gap the fill has crossed.\n */\n --meter-edge: calc(\n (100% - (var(--meter-segments) - 1) * var(--vectis-control-size-meter-segment-gap)) *\n var(--meter-fraction) + var(--meter-gaps-filled) *\n var(--vectis-control-size-meter-segment-gap)\n );\n position: relative;\n display: flex;\n gap: var(--vectis-control-size-meter-segment-gap);\n block-size: var(--meter-thickness);\n }\n\n .v-meter-segment {\n position: relative;\n flex: 1 1 0;\n border-radius: var(--vectis-radius-pill);\n background: var(--meter-track);\n }\n\n .v-meter-fill {\n position: absolute;\n inset-block: 0;\n inset-inline-start: 0;\n inline-size: calc(100% * var(--meter-segment-fill));\n border-radius: inherit;\n background: var(--meter-fill);\n /* A logical length rather than a scale, which would have to be undone in right to left. */\n transition:\n inline-size var(--vectis-duration-base) var(--vectis-ease-default),\n background-color var(--vectis-duration-base) var(--vectis-ease-default);\n }\n\n /*\n * The two copies are complementary, each cut to its own side of the fill's edge, so every\n * letter is painted once. The large negative insets keep text taller than the bar uncropped.\n */\n .v-meter-text {\n --meter-clip-start: var(--meter-edge);\n --meter-clip-end: -100vmax;\n position: absolute;\n inset: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n padding-inline: var(--vectis-space-2);\n color: var(--vectis-color-text);\n font-size: var(--vectis-text-caption-size);\n font-weight: var(--vectis-text-control-weight);\n line-height: var(--vectis-text-control-leading);\n font-variant-numeric: tabular-nums;\n clip-path: inset(-100vmax var(--meter-clip-end) -100vmax var(--meter-clip-start));\n transition: clip-path var(--vectis-duration-base) var(--vectis-ease-default);\n white-space: nowrap;\n pointer-events: none;\n }\n\n .v-meter[data-value-position='start'] .v-meter-text {\n justify-content: start;\n }\n\n .v-meter[data-value-position='end'] .v-meter-text {\n justify-content: end;\n }\n\n /* The fallback stands alone for the reason given in VProgressLinear: a var() in a second\n declaration would win the cascade and then turn invalid. */\n .v-meter-text[data-on-fill] {\n --meter-clip-start: -100vmax;\n --meter-clip-end: calc(100% - var(--meter-edge));\n color: var(--meter-text-fallback);\n }\n\n @supports (color: contrast-color(red)) {\n .v-meter-text[data-on-fill] {\n color: contrast-color(var(--meter-fill));\n }\n }\n\n .v-meter-track:dir(rtl) .v-meter-text {\n clip-path: inset(-100vmax var(--meter-clip-start) -100vmax var(--meter-clip-end));\n }\n\n /*\n * As in VProgressLinear, the track turns on its side through its writing mode, so every inline\n * length and inset above follows. Zero goes to the bottom by anchoring the first segment and\n * each fill at the far end of the axis, never by reversing the direction, which would reorder\n * the text's characters.\n */\n .v-meter[data-orientation='vertical'] .v-meter-track {\n flex: 1 1 0;\n flex-direction: row-reverse;\n writing-mode: vertical-lr;\n }\n\n .v-meter[data-orientation='vertical'] .v-meter-fill {\n inset-inline-start: auto;\n inset-inline-end: 0;\n }\n\n /* A right-to-left page already runs a vertical inline axis from the bottom up. */\n .v-meter[data-orientation='vertical'] .v-meter-track:dir(rtl) {\n flex-direction: row;\n }\n\n .v-meter[data-orientation='vertical'] .v-meter-track:dir(rtl) .v-meter-fill {\n inset-inline-start: 0;\n inset-inline-end: auto;\n }\n\n .v-meter[data-orientation='vertical'] .v-meter-text {\n flex-direction: column-reverse;\n padding-inline: 0;\n padding-block: var(--vectis-space-2);\n /* The text stays upright on a bar turned on its side. */\n writing-mode: horizontal-tb;\n }\n\n /* The second selector outranks the right-to-left rule above instead of tying with it. */\n .v-meter[data-orientation='vertical'] .v-meter-text,\n .v-meter[data-orientation='vertical'] .v-meter-track:dir(rtl) .v-meter-text {\n clip-path: inset(var(--meter-clip-end) -100vmax var(--meter-clip-start) -100vmax);\n }\n\n @media (prefers-reduced-motion: reduce) {\n .v-meter-fill,\n .v-meter-text {\n transition: none;\n }\n }\n\n @media (forced-colors: active) {\n .v-meter-segment {\n forced-color-adjust: none;\n background: Canvas;\n box-shadow: inset 0 0 0 var(--vectis-control-border-width) CanvasText;\n }\n\n .v-meter-fill {\n background: Highlight;\n }\n\n .v-meter-text {\n forced-color-adjust: none;\n color: CanvasText;\n }\n\n .v-meter-text[data-on-fill] {\n color: HighlightText;\n }\n }\n}\n</style>\n"],"mappings":""}
|