@pgcorp/ui-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +157 -0
- package/docs/accessibility.md +27 -0
- package/docs/getting-started.md +80 -0
- package/docs/licensing.md +30 -0
- package/docs/public-api.md +39 -0
- package/docs/security.md +21 -0
- package/docs/theming.md +35 -0
- package/docs/versioning.md +23 -0
- package/index.ts +3 -0
- package/package.json +592 -0
- package/src/components/icons/_internal/generatedSputnigUiIconMarkup.ts +971 -0
- package/src/components/icons/sputnigUiIcons.ts +530 -0
- package/src/components/layout/SAppShell.vue +232 -0
- package/src/components/layout/SDockRegion.vue +200 -0
- package/src/components/layout/SResizeHandle.vue +251 -0
- package/src/components/layout/SSideMenuButton.vue +104 -0
- package/src/components/layout/SSideMenuRail.vue +47 -0
- package/src/components/layout/SStack.css +25 -0
- package/src/components/layout/SStack.vue +57 -0
- package/src/components/layout/SStatusBarShell.css +15 -0
- package/src/components/layout/SStatusBarShell.vue +26 -0
- package/src/components/layout/SWorkbenchLayout.vue +53 -0
- package/src/components/layout/SWorkbenchPanelShell.css +161 -0
- package/src/components/layout/SWorkbenchPanelShell.vue +286 -0
- package/src/components/layout/_internal/SAppBrand.css +45 -0
- package/src/components/layout/_internal/SAppBrand.vue +37 -0
- package/src/components/layout/_internal/SAppBrandContent.vue +30 -0
- package/src/components/layout/types.ts +28 -0
- package/src/components/shared/_internal/SDialogSurface.css +127 -0
- package/src/components/shared/_internal/SDialogSurface.vue +117 -0
- package/src/components/shared/_internal/SDisclosureSummary.css +92 -0
- package/src/components/shared/_internal/SDisclosureSummary.vue +121 -0
- package/src/components/shared/_internal/SListboxGroup.css +18 -0
- package/src/components/shared/_internal/SListboxGroup.vue +24 -0
- package/src/components/shared/_internal/SMenuItem.css +112 -0
- package/src/components/shared/_internal/SMenuItem.vue +213 -0
- package/src/components/shared/_internal/SMenuLayer.css +21 -0
- package/src/components/shared/_internal/SMenuLayer.vue +48 -0
- package/src/components/shared/_internal/SMenuSeparator.css +5 -0
- package/src/components/shared/_internal/SMenuSeparator.vue +13 -0
- package/src/components/shared/_internal/SToastItem.css +65 -0
- package/src/components/shared/_internal/SToastItem.vue +58 -0
- package/src/components/shared/_internal/STreeNode.vue +761 -0
- package/src/components/shared/_internal/dialogSurface.ts +105 -0
- package/src/components/shared/_internal/focusTrap.ts +40 -0
- package/src/components/shared/_internal/listboxContext.ts +16 -0
- package/src/components/shared/_internal/markerContract.ts +10 -0
- package/src/components/shared/_internal/menuContext.ts +23 -0
- package/src/components/shared/_internal/selectionContract.ts +239 -0
- package/src/components/shared/_internal/sidebarGroupContext.ts +31 -0
- package/src/components/shared/_internal/tabsContext.ts +57 -0
- package/src/components/shared/_internal/treeRuntime.ts +203 -0
- package/src/components/shared/_internal/useSelectionRoving.ts +162 -0
- package/src/components/shared/codeLanguages.ts +168 -0
- package/src/components/shared/complex/SKanbanCard.css +8 -0
- package/src/components/shared/complex/SKanbanCard.vue +76 -0
- package/src/components/shared/complex/SKanbanLane.vue +82 -0
- package/src/components/shared/complex/STree.css +24 -0
- package/src/components/shared/complex/STree.vue +288 -0
- package/src/components/shared/complex/treeAdapter.ts +210 -0
- package/src/components/shared/complex/types.ts +102 -0
- package/src/components/shared/containers/SCloudChrome.css +103 -0
- package/src/components/shared/containers/SCloudChrome.vue +87 -0
- package/src/components/shared/containers/SCloudChromeEmpty.css +10 -0
- package/src/components/shared/containers/SCloudChromeEmpty.vue +18 -0
- package/src/components/shared/containers/SCloudChromeRow.css +78 -0
- package/src/components/shared/containers/SCloudChromeRow.vue +108 -0
- package/src/components/shared/containers/SCollapsiblePanel.css +57 -0
- package/src/components/shared/containers/SCollapsiblePanel.vue +360 -0
- package/src/components/shared/containers/SDrawer.css +60 -0
- package/src/components/shared/containers/SDrawer.vue +86 -0
- package/src/components/shared/containers/SFormGrid.css +26 -0
- package/src/components/shared/containers/SFormGrid.vue +46 -0
- package/src/components/shared/containers/SFormGridItem.css +18 -0
- package/src/components/shared/containers/SFormGridItem.vue +29 -0
- package/src/components/shared/containers/SLeftSidebar.vue +74 -0
- package/src/components/shared/containers/SModal.css +27 -0
- package/src/components/shared/containers/SModal.vue +105 -0
- package/src/components/shared/containers/SPageHeader.css +66 -0
- package/src/components/shared/containers/SPageHeader.vue +45 -0
- package/src/components/shared/containers/SPanel.css +134 -0
- package/src/components/shared/containers/SPanel.vue +169 -0
- package/src/components/shared/containers/SPopover.css +35 -0
- package/src/components/shared/containers/SPopover.vue +551 -0
- package/src/components/shared/containers/SSidebarGroup.css +12 -0
- package/src/components/shared/containers/SSidebarGroup.vue +443 -0
- package/src/components/shared/containers/SSidebarSection.css +41 -0
- package/src/components/shared/containers/SSidebarSection.vue +268 -0
- package/src/components/shared/containers/disclosureSummary.ts +48 -0
- package/src/components/shared/containers/popover.ts +72 -0
- package/src/components/shared/containers/sidebar.ts +206 -0
- package/src/components/shared/controls/SButton.css +274 -0
- package/src/components/shared/controls/SButton.vue +211 -0
- package/src/components/shared/controls/SCheckbox.css +65 -0
- package/src/components/shared/controls/SCheckbox.vue +130 -0
- package/src/components/shared/controls/SCheckboxGroup.vue +134 -0
- package/src/components/shared/controls/SChoiceCards.css +29 -0
- package/src/components/shared/controls/SChoiceCards.vue +145 -0
- package/src/components/shared/controls/SColorSelect.vue +146 -0
- package/src/components/shared/controls/SComboboxTrigger.css +35 -0
- package/src/components/shared/controls/SComboboxTrigger.vue +122 -0
- package/src/components/shared/controls/SContextMenu.css +11 -0
- package/src/components/shared/controls/SContextMenu.vue +186 -0
- package/src/components/shared/controls/SContextToggleButton.vue +64 -0
- package/src/components/shared/controls/SDragHandle.vue +100 -0
- package/src/components/shared/controls/SDropdownMenu.css +5 -0
- package/src/components/shared/controls/SDropdownMenu.vue +188 -0
- package/src/components/shared/controls/SField.css +79 -0
- package/src/components/shared/controls/SField.vue +89 -0
- package/src/components/shared/controls/SFieldGroup.css +58 -0
- package/src/components/shared/controls/SFieldGroup.vue +117 -0
- package/src/components/shared/controls/SFieldLabel.css +12 -0
- package/src/components/shared/controls/SFieldLabel.vue +51 -0
- package/src/components/shared/controls/SInlineTokenEditor.css +61 -0
- package/src/components/shared/controls/SInlineTokenEditor.vue +1081 -0
- package/src/components/shared/controls/SInputText.css +66 -0
- package/src/components/shared/controls/SInputText.vue +321 -0
- package/src/components/shared/controls/SInteractiveSurface.css +72 -0
- package/src/components/shared/controls/SInteractiveSurface.vue +326 -0
- package/src/components/shared/controls/SLink.css +114 -0
- package/src/components/shared/controls/SLink.ts +85 -0
- package/src/components/shared/controls/SLink.vue +114 -0
- package/src/components/shared/controls/SListbox.css +31 -0
- package/src/components/shared/controls/SListbox.vue +388 -0
- package/src/components/shared/controls/SListboxOption.css +63 -0
- package/src/components/shared/controls/SListboxOption.vue +85 -0
- package/src/components/shared/controls/SMarker.css +19 -0
- package/src/components/shared/controls/SMarker.vue +33 -0
- package/src/components/shared/controls/SMenuSurface.css +20 -0
- package/src/components/shared/controls/SMenuSurface.vue +333 -0
- package/src/components/shared/controls/SSegmentedControl.css +38 -0
- package/src/components/shared/controls/SSegmentedControl.vue +134 -0
- package/src/components/shared/controls/SSelect.css +12 -0
- package/src/components/shared/controls/SSelect.vue +338 -0
- package/src/components/shared/controls/STextarea.css +31 -0
- package/src/components/shared/controls/STextarea.vue +316 -0
- package/src/components/shared/controls/_internal/SInlineTokenSurface.vue +224 -0
- package/src/components/shared/controls/menu.ts +178 -0
- package/src/components/shared/data-display/SActionCard.css +6 -0
- package/src/components/shared/data-display/SActionCard.vue +49 -0
- package/src/components/shared/data-display/SActionList.css +15 -0
- package/src/components/shared/data-display/SActionList.vue +34 -0
- package/src/components/shared/data-display/SActionListItem.css +114 -0
- package/src/components/shared/data-display/SActionListItem.vue +230 -0
- package/src/components/shared/data-display/SBadge.css +73 -0
- package/src/components/shared/data-display/SBadge.vue +52 -0
- package/src/components/shared/data-display/SCodeBlock.css +56 -0
- package/src/components/shared/data-display/SCodeBlock.vue +188 -0
- package/src/components/shared/data-display/SCodeEditor.css +178 -0
- package/src/components/shared/data-display/SCodeEditor.vue +274 -0
- package/src/components/shared/data-display/SCodeSearchPanel.vue +327 -0
- package/src/components/shared/data-display/SCopyField.vue +77 -0
- package/src/components/shared/data-display/SDocBlock.css +136 -0
- package/src/components/shared/data-display/SDocBlock.vue +358 -0
- package/src/components/shared/data-display/SEmptyState.css +42 -0
- package/src/components/shared/data-display/SEmptyState.vue +79 -0
- package/src/components/shared/data-display/SExpandableText.vue +314 -0
- package/src/components/shared/data-display/SJsonTree.css +6 -0
- package/src/components/shared/data-display/SJsonTree.vue +111 -0
- package/src/components/shared/data-display/SKeyValueGrid.css +37 -0
- package/src/components/shared/data-display/SKeyValueGrid.vue +277 -0
- package/src/components/shared/data-display/SLinkedSystemsList.vue +270 -0
- package/src/components/shared/data-display/SMessage.css +88 -0
- package/src/components/shared/data-display/SMessage.vue +152 -0
- package/src/components/shared/data-display/SMetricCard.css +32 -0
- package/src/components/shared/data-display/SMetricCard.vue +117 -0
- package/src/components/shared/data-display/SProgressBar.css +84 -0
- package/src/components/shared/data-display/SProgressBar.vue +114 -0
- package/src/components/shared/data-display/SProgressIndicator.css +59 -0
- package/src/components/shared/data-display/SProgressIndicator.vue +104 -0
- package/src/components/shared/data-display/SSectionHeader.css +84 -0
- package/src/components/shared/data-display/SSectionHeader.vue +58 -0
- package/src/components/shared/data-display/SStatus.css +90 -0
- package/src/components/shared/data-display/SStatus.vue +79 -0
- package/src/components/shared/data-display/STable.css +83 -0
- package/src/components/shared/data-display/STable.vue +339 -0
- package/src/components/shared/data-display/SToastContainer.css +53 -0
- package/src/components/shared/data-display/SToastContainer.vue +31 -0
- package/src/components/shared/data-display/STooltip.css +114 -0
- package/src/components/shared/data-display/STooltip.vue +550 -0
- package/src/components/shared/data-display/STooltipAnchor.css +21 -0
- package/src/components/shared/data-display/STooltipAnchor.vue +119 -0
- package/src/components/shared/data-display/SVirtualList.css +24 -0
- package/src/components/shared/data-display/SVirtualList.ts +19 -0
- package/src/components/shared/data-display/SVirtualList.vue +361 -0
- package/src/components/shared/data-display/json.ts +136 -0
- package/src/components/shared/data-display/table.ts +76 -0
- package/src/components/shared/database/SDataGrid.css +3 -0
- package/src/components/shared/database/SDataGrid.vue +464 -0
- package/src/components/shared/database/SSqlEditor.vue +363 -0
- package/src/components/shared/database/dataGrid.ts +61 -0
- package/src/components/shared/feedback/SAsyncState.css +26 -0
- package/src/components/shared/feedback/SAsyncState.vue +190 -0
- package/src/components/shared/graph/SGraphViewport.ts +1 -0
- package/src/components/shared/graph/SGraphViewport.vue +858 -0
- package/src/components/shared/graph/_internal/graphViewportContract.ts +362 -0
- package/src/components/shared/markdown.ts +169 -0
- package/src/components/shared/navigation/SBottomNav.vue +692 -0
- package/src/components/shared/navigation/SBreadcrumbs.css +26 -0
- package/src/components/shared/navigation/SBreadcrumbs.vue +155 -0
- package/src/components/shared/navigation/SCatalogNavigator.css +100 -0
- package/src/components/shared/navigation/SCatalogNavigator.vue +308 -0
- package/src/components/shared/navigation/STab.vue +363 -0
- package/src/components/shared/navigation/STabList.vue +442 -0
- package/src/components/shared/navigation/STabPanel.vue +165 -0
- package/src/components/shared/navigation/STabPanels.vue +33 -0
- package/src/components/shared/navigation/STabs.vue +617 -0
- package/src/components/shared/navigation/SWizardSteps.css +69 -0
- package/src/components/shared/navigation/SWizardSteps.vue +171 -0
- package/src/components/shared/navigation/_internal/SCatalogNavigatorGroup.vue +63 -0
- package/src/components/shared/persona/SPersonaProjectPicker.css +36 -0
- package/src/components/shared/persona/SPersonaProjectPicker.vue +361 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.css +109 -0
- package/src/components/shared/persona/SPersonaRunWorkbench.vue +549 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.css +40 -0
- package/src/components/shared/persona/_internal/SPersonaRunConsole.vue +298 -0
- package/src/components/shared/persona/_internal/inventory.ts +110 -0
- package/src/components/shared/persona/_internal/labels.ts +79 -0
- package/src/components/shared/persona/presentation.ts +371 -0
- package/src/components/shared/persona/types.ts +256 -0
- package/src/composables/uiPreferencesReset.ts +52 -0
- package/src/composables/useClipboard.ts +37 -0
- package/src/composables/useDialogOverlay.ts +85 -0
- package/src/composables/useDisclosure.ts +40 -0
- package/src/composables/useFloatingPosition.ts +245 -0
- package/src/composables/useNotifier.ts +26 -0
- package/src/composables/useSidebarPanelState.ts +610 -0
- package/src/internal/codeEditorContract.ts +110 -0
- package/src/internal/codeEditorExtensions.ts +27 -0
- package/src/internal/feedbackPresentation.ts +136 -0
- package/src/internal/fieldSurface.ts +59 -0
- package/src/internal/focusNavigation.ts +69 -0
- package/src/internal/inlineTokenEditorContract.ts +136 -0
- package/src/internal/interactiveElement.ts +33 -0
- package/src/internal/layerStack.ts +202 -0
- package/src/internal/linkTarget.ts +256 -0
- package/src/internal/ownedAttrs.ts +464 -0
- package/src/internal/passiveContentContract.ts +129 -0
- package/src/internal/pointerInteractionLease.ts +81 -0
- package/src/internal/runtimeContract.ts +194 -0
- package/src/internal/runtimeTheme.ts +159 -0
- package/src/internal/semanticSizing.ts +60 -0
- package/src/internal/useMenuLayer.ts +129 -0
- package/src/stores/useNotifierStore.ts +69 -0
- package/src/styles/reference.css +4 -0
- package/src/styles/style.css +151 -0
- package/src/styles/tokens.css +836 -0
- package/src/theme.ts +449 -0
- package/src/types/highlight-js-lib.d.ts +76 -0
- package/src/variants.ts +15 -0
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<ul
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
:id="resolvedId"
|
|
5
|
+
class="s-listbox"
|
|
6
|
+
:data-variant="resolvedVariant"
|
|
7
|
+
:data-invalid="resolvedInvalid || undefined"
|
|
8
|
+
:data-readonly="resolvedReadonly || undefined"
|
|
9
|
+
role="listbox"
|
|
10
|
+
:aria-label="resolvedLabel"
|
|
11
|
+
:aria-multiselectable="resolvedSelectionMode === 'multiple' || undefined"
|
|
12
|
+
:aria-disabled="resolvedDisabled || undefined"
|
|
13
|
+
:aria-readonly="resolvedReadonly || undefined"
|
|
14
|
+
:aria-invalid="resolvedInvalid || undefined"
|
|
15
|
+
@keydown="handleKeydown"
|
|
16
|
+
>
|
|
17
|
+
<SListboxOption
|
|
18
|
+
v-if="resolvedClearOptionLabel"
|
|
19
|
+
:registration-token="CLEAR_OPTION_TOKEN"
|
|
20
|
+
:label="resolvedClearOptionLabel"
|
|
21
|
+
:title="resolvedClearOptionLabel"
|
|
22
|
+
:selected="modelValue === null"
|
|
23
|
+
:disabled="resolvedDisabled"
|
|
24
|
+
:appearance="optionAppearance"
|
|
25
|
+
@select="clearSelection"
|
|
26
|
+
/>
|
|
27
|
+
<template v-for="entry in inventory.entries" :key="entry.token">
|
|
28
|
+
<SListboxOption
|
|
29
|
+
v-if="entry.kind === 'option'"
|
|
30
|
+
:registration-token="entry.record.token"
|
|
31
|
+
:label="entry.record.option.label"
|
|
32
|
+
:title="entry.record.option.title ?? entry.record.option.label"
|
|
33
|
+
:selected="selectedTokens.has(entry.record.token)"
|
|
34
|
+
:disabled="resolvedDisabled || entry.record.option.disabled"
|
|
35
|
+
:marker="entry.record.option.marker"
|
|
36
|
+
:appearance="optionAppearance"
|
|
37
|
+
@select="selectOption(entry.record.token)"
|
|
38
|
+
/>
|
|
39
|
+
<SListboxGroup
|
|
40
|
+
v-else
|
|
41
|
+
:label="entry.label"
|
|
42
|
+
:label-id="groupLabelId(entry.token)"
|
|
43
|
+
:variant="resolvedVariant"
|
|
44
|
+
>
|
|
45
|
+
<SListboxOption
|
|
46
|
+
v-for="record in entry.records"
|
|
47
|
+
:key="record.token"
|
|
48
|
+
:registration-token="record.token"
|
|
49
|
+
:label="record.option.label"
|
|
50
|
+
:title="record.option.title ?? record.option.label"
|
|
51
|
+
:selected="selectedTokens.has(record.token)"
|
|
52
|
+
:disabled="resolvedDisabled || record.option.disabled"
|
|
53
|
+
:marker="record.option.marker"
|
|
54
|
+
:appearance="optionAppearance"
|
|
55
|
+
@select="selectOption(record.token)"
|
|
56
|
+
/>
|
|
57
|
+
</SListboxGroup>
|
|
58
|
+
</template>
|
|
59
|
+
<li v-if="inventory.options.length === 0 && !resolvedClearOptionLabel" class="s-listbox__empty" role="presentation">
|
|
60
|
+
{{ resolvedEmptyLabel }}
|
|
61
|
+
</li>
|
|
62
|
+
</ul>
|
|
63
|
+
</template>
|
|
64
|
+
|
|
65
|
+
<script lang="ts">
|
|
66
|
+
import type { SelectionKey as PublicSelectionKey } from '../_internal/useSelectionRoving'
|
|
67
|
+
import type {
|
|
68
|
+
SelectionEntry as PublicSelectionEntry,
|
|
69
|
+
SelectionMode as PublicSelectionMode,
|
|
70
|
+
SelectionModelValue as PublicSelectionModelValue,
|
|
71
|
+
SelectionOption as PublicSelectionOption,
|
|
72
|
+
SelectionOptionGroup as PublicSelectionOptionGroup,
|
|
73
|
+
} from '../_internal/selectionContract'
|
|
74
|
+
|
|
75
|
+
export type SListboxVariant = 'select' | 'color'
|
|
76
|
+
export type SListboxOption<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOption<TKey>
|
|
77
|
+
export type SListboxOptionGroup<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionOptionGroup<TKey>
|
|
78
|
+
export type SListboxEntry<TKey extends PublicSelectionKey = PublicSelectionKey> = PublicSelectionEntry<TKey>
|
|
79
|
+
export type SListboxMode = PublicSelectionMode
|
|
80
|
+
export type SListboxModelValue<
|
|
81
|
+
TKey extends PublicSelectionKey,
|
|
82
|
+
TMode extends SListboxMode,
|
|
83
|
+
> = PublicSelectionModelValue<TKey, TMode>
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Точная identity option для keyboard event, которым владеет SListbox.
|
|
87
|
+
* Exact option identity for a keyboard event owned by SListbox.
|
|
88
|
+
* Consumer может вызвать preventDefault(), чтобы забрать augmented keyboard chord
|
|
89
|
+
* до базовой keyboard-модели listbox. / A consumer may call preventDefault() to claim an
|
|
90
|
+
* augmented keyboard chord before the base listbox keyboard model runs.
|
|
91
|
+
*/
|
|
92
|
+
export interface SListboxOptionKeydownPayload<
|
|
93
|
+
TKey extends PublicSelectionKey = PublicSelectionKey,
|
|
94
|
+
> {
|
|
95
|
+
readonly value: TKey | null
|
|
96
|
+
readonly event: KeyboardEvent
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface SharedProps<TKey extends PublicSelectionKey> {
|
|
100
|
+
options: readonly SListboxEntry<TKey>[]
|
|
101
|
+
label: string
|
|
102
|
+
disabled?: boolean
|
|
103
|
+
readonly?: boolean
|
|
104
|
+
invalid?: boolean
|
|
105
|
+
variant?: SListboxVariant
|
|
106
|
+
emptyLabel?: string
|
|
107
|
+
clearOptionLabel?: string
|
|
108
|
+
id?: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type SListboxProps<
|
|
112
|
+
TKey extends PublicSelectionKey = PublicSelectionKey,
|
|
113
|
+
TMode extends SListboxMode = 'single',
|
|
114
|
+
> = SharedProps<TKey> & (TMode extends 'multiple'
|
|
115
|
+
? { selectionMode: 'multiple'; modelValue: readonly TKey[]; clearOptionLabel?: never }
|
|
116
|
+
: { selectionMode?: 'single'; modelValue: TKey | null; clearOptionLabel?: string })
|
|
117
|
+
|
|
118
|
+
type ComponentProps<TKey extends PublicSelectionKey> = SharedProps<TKey> & (
|
|
119
|
+
| { selectionMode?: 'single'; modelValue: TKey | null; clearOptionLabel?: string }
|
|
120
|
+
| { selectionMode: 'multiple'; modelValue: readonly TKey[]; clearOptionLabel?: never }
|
|
121
|
+
)
|
|
122
|
+
</script>
|
|
123
|
+
|
|
124
|
+
<script setup lang="ts" generic="TKey extends SelectionKey = string">
|
|
125
|
+
import { computed, onBeforeUnmount, provide, useId } from 'vue'
|
|
126
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
127
|
+
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
128
|
+
import {
|
|
129
|
+
validateBoolean,
|
|
130
|
+
validateDomId,
|
|
131
|
+
validateExactString,
|
|
132
|
+
validateNonEmptyString,
|
|
133
|
+
} from '../../../internal/runtimeContract'
|
|
134
|
+
import {
|
|
135
|
+
isSelectionModelList,
|
|
136
|
+
resolveSelectionInventory,
|
|
137
|
+
validateSelectionModel,
|
|
138
|
+
type SelectionMode,
|
|
139
|
+
} from '../_internal/selectionContract'
|
|
140
|
+
import { listboxContextKey } from '../_internal/listboxContext'
|
|
141
|
+
import type { ListboxOptionRegistration } from '../_internal/listboxContext'
|
|
142
|
+
import { selectionKeyToken, type SelectionKey } from '../_internal/useSelectionRoving'
|
|
143
|
+
import SListboxGroup from '../_internal/SListboxGroup.vue'
|
|
144
|
+
import SListboxOption from './SListboxOption.vue'
|
|
145
|
+
|
|
146
|
+
defineOptions({ inheritAttrs: false })
|
|
147
|
+
|
|
148
|
+
const props = withDefaults(defineProps<ComponentProps<TKey>>(), {
|
|
149
|
+
selectionMode: undefined,
|
|
150
|
+
disabled: false,
|
|
151
|
+
readonly: false,
|
|
152
|
+
invalid: false,
|
|
153
|
+
variant: 'select',
|
|
154
|
+
emptyLabel: 'Нет доступных вариантов',
|
|
155
|
+
clearOptionLabel: undefined,
|
|
156
|
+
id: undefined,
|
|
157
|
+
})
|
|
158
|
+
const emit = defineEmits<{
|
|
159
|
+
'update:modelValue': [value: TKey | null] | [value: readonly TKey[]]
|
|
160
|
+
'option-keydown': [payload: SListboxOptionKeydownPayload<TKey>]
|
|
161
|
+
escape: []
|
|
162
|
+
tab: [event: KeyboardEvent]
|
|
163
|
+
}>()
|
|
164
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SListbox', owner: 'listbox root' })
|
|
165
|
+
useInteractiveLeafRegistration({ owner: 'SListbox' })
|
|
166
|
+
const generatedId = useId()
|
|
167
|
+
const contract = computed(() => {
|
|
168
|
+
const selectionMode: SelectionMode = validateExactString(
|
|
169
|
+
'SListbox', 'selectionMode', props.selectionMode ?? 'single', ['single', 'multiple'] as const,
|
|
170
|
+
)
|
|
171
|
+
let clearOptionLabel: string | undefined
|
|
172
|
+
if (props.clearOptionLabel !== undefined) {
|
|
173
|
+
if (selectionMode === 'multiple') {
|
|
174
|
+
throw new Error('SListbox: clearOptionLabel is available only in single selection mode')
|
|
175
|
+
}
|
|
176
|
+
clearOptionLabel = validateNonEmptyString('SListbox', 'clearOptionLabel', props.clearOptionLabel)
|
|
177
|
+
}
|
|
178
|
+
return {
|
|
179
|
+
id: props.id === undefined
|
|
180
|
+
? `s-listbox-${generatedId}`
|
|
181
|
+
: validateDomId('SListbox', 'id', props.id),
|
|
182
|
+
label: validateNonEmptyString('SListbox', 'label', props.label),
|
|
183
|
+
emptyLabel: validateNonEmptyString('SListbox', 'emptyLabel', props.emptyLabel),
|
|
184
|
+
variant: validateExactString(
|
|
185
|
+
'SListbox', 'variant', props.variant, ['select', 'color'] as const,
|
|
186
|
+
),
|
|
187
|
+
disabled: validateBoolean('SListbox', 'disabled', props.disabled),
|
|
188
|
+
readonly: validateBoolean('SListbox', 'readonly', props.readonly),
|
|
189
|
+
invalid: validateBoolean('SListbox', 'invalid', props.invalid),
|
|
190
|
+
selectionMode,
|
|
191
|
+
clearOptionLabel,
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
const resolvedId = computed(() => contract.value.id)
|
|
195
|
+
const resolvedLabel = computed(() => contract.value.label)
|
|
196
|
+
const resolvedEmptyLabel = computed(() => contract.value.emptyLabel)
|
|
197
|
+
const resolvedVariant = computed(() => contract.value.variant)
|
|
198
|
+
const resolvedDisabled = computed(() => contract.value.disabled)
|
|
199
|
+
const resolvedReadonly = computed(() => contract.value.readonly)
|
|
200
|
+
const resolvedInvalid = computed(() => contract.value.invalid)
|
|
201
|
+
const resolvedSelectionMode = computed(() => contract.value.selectionMode)
|
|
202
|
+
const resolvedClearOptionLabel = computed(() => contract.value.clearOptionLabel)
|
|
203
|
+
const selectionState = computed(() => {
|
|
204
|
+
const resolvedInventory = resolveSelectionInventory<TKey>('SListbox', props.options)
|
|
205
|
+
return {
|
|
206
|
+
inventory: resolvedInventory,
|
|
207
|
+
selectedTokens: validateSelectionModel(
|
|
208
|
+
'SListbox', contract.value.selectionMode, props.modelValue, resolvedInventory,
|
|
209
|
+
),
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
const inventory = computed(() => selectionState.value.inventory)
|
|
213
|
+
const selectedTokens = computed(() => selectionState.value.selectedTokens)
|
|
214
|
+
const optionAppearance = computed(() => resolvedVariant.value === 'color' ? 'color' : 'select')
|
|
215
|
+
const CLEAR_OPTION_TOKEN = 'clear-option'
|
|
216
|
+
|
|
217
|
+
function emitModelValue(value: TKey | null | readonly TKey[]): void {
|
|
218
|
+
if (contract.value.selectionMode === 'multiple') {
|
|
219
|
+
if (!isSelectionModelList(value)) {
|
|
220
|
+
throw new TypeError('SListbox: multiple selection update must be an array')
|
|
221
|
+
}
|
|
222
|
+
validateSelectionModel('SListbox update', 'multiple', value, inventory.value)
|
|
223
|
+
emit('update:modelValue', value)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (isSelectionModelList(value)) {
|
|
227
|
+
throw new TypeError('SListbox: single selection update must be a selection key or null')
|
|
228
|
+
}
|
|
229
|
+
validateSelectionModel('SListbox update', 'single', value, inventory.value)
|
|
230
|
+
emit('update:modelValue', value)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function groupLabelId(token: string): string {
|
|
234
|
+
return `${resolvedId.value}-${token.replace(/[^a-zA-Z0-9_-]/gu, '-')}-label`
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function selectOption(token: string): void {
|
|
238
|
+
if (resolvedDisabled.value || resolvedReadonly.value) return
|
|
239
|
+
const record = inventory.value.optionsByToken.get(token)
|
|
240
|
+
if (!record) throw new Error('SListbox: attempted to select a stale option identity')
|
|
241
|
+
if (record.option.disabled) return
|
|
242
|
+
if (resolvedSelectionMode.value === 'single') {
|
|
243
|
+
emitModelValue(record.option.value)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
if (!Array.isArray(props.modelValue)) {
|
|
247
|
+
throw new TypeError('SListbox: multiple selection requires modelValue to be an array')
|
|
248
|
+
}
|
|
249
|
+
const current = [...props.modelValue]
|
|
250
|
+
const currentIndex = current.findIndex((value) => (
|
|
251
|
+
selectionKeyToken('SListbox', value, 'modelValue item') === token
|
|
252
|
+
))
|
|
253
|
+
if (currentIndex >= 0) current.splice(currentIndex, 1)
|
|
254
|
+
else current.push(record.option.value)
|
|
255
|
+
emitModelValue(current)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function clearSelection(): void {
|
|
259
|
+
if (resolvedDisabled.value || resolvedReadonly.value) return
|
|
260
|
+
if (resolvedSelectionMode.value !== 'single') {
|
|
261
|
+
throw new Error('SListbox: clear selection is available only in single selection mode')
|
|
262
|
+
}
|
|
263
|
+
emitModelValue(null)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const registrations = new Set<ListboxOptionRegistration>()
|
|
267
|
+
let typeaheadBuffer = ''
|
|
268
|
+
let typeaheadTimer: ReturnType<typeof setTimeout> | undefined
|
|
269
|
+
|
|
270
|
+
function enabledRegistrations(): ListboxOptionRegistration[] {
|
|
271
|
+
return [...registrations]
|
|
272
|
+
.filter((option) => !option.disabled() && option.element.isConnected)
|
|
273
|
+
.sort((left, right) => {
|
|
274
|
+
const position = left.element.compareDocumentPosition(right.element)
|
|
275
|
+
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1
|
|
276
|
+
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1
|
|
277
|
+
return 0
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function initializeTabstop(): void {
|
|
282
|
+
const options = enabledRegistrations()
|
|
283
|
+
for (const option of registrations) option.element.tabIndex = -1
|
|
284
|
+
const selected = options.find((option) => option.selected())
|
|
285
|
+
const target = selected ?? options[0]
|
|
286
|
+
if (target) target.element.tabIndex = 0
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
provide(listboxContextKey, {
|
|
290
|
+
register: (registration) => {
|
|
291
|
+
registrations.add(registration)
|
|
292
|
+
queueMicrotask(initializeTabstop)
|
|
293
|
+
return () => registrations.delete(registration)
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
|
|
297
|
+
function focusOption(target: ListboxOptionRegistration, options = enabledRegistrations()): void {
|
|
298
|
+
for (const option of registrations) option.element.tabIndex = -1
|
|
299
|
+
target.element.tabIndex = 0
|
|
300
|
+
target.element.focus()
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function currentOption(
|
|
304
|
+
options: ListboxOptionRegistration[],
|
|
305
|
+
event: KeyboardEvent,
|
|
306
|
+
): ListboxOptionRegistration | undefined {
|
|
307
|
+
return options.find((option) => option.element === event.target)
|
|
308
|
+
?? options.find((option) => option.element === document.activeElement)
|
|
309
|
+
?? options.find((option) => option.element.tabIndex === 0)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function optionValue(registration: ListboxOptionRegistration): TKey | null {
|
|
313
|
+
if (registration.token === CLEAR_OPTION_TOKEN) return null
|
|
314
|
+
const record = inventory.value.optionsByToken.get(registration.token)
|
|
315
|
+
if (!record) {
|
|
316
|
+
throw new Error(`SListbox: registered option token ${JSON.stringify(registration.token)} is stale`)
|
|
317
|
+
}
|
|
318
|
+
return record.option.value
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function handleKeydown(event: KeyboardEvent): void {
|
|
322
|
+
if (event.key === 'Tab') {
|
|
323
|
+
emit('tab', event)
|
|
324
|
+
return
|
|
325
|
+
}
|
|
326
|
+
if (event.key === 'Escape') {
|
|
327
|
+
event.preventDefault()
|
|
328
|
+
emit('escape')
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
if (resolvedDisabled.value) return
|
|
332
|
+
const options = enabledRegistrations()
|
|
333
|
+
const activeOption = currentOption(options, event)
|
|
334
|
+
if (activeOption) {
|
|
335
|
+
emit('option-keydown', { value: optionValue(activeOption), event })
|
|
336
|
+
if (event.defaultPrevented) return
|
|
337
|
+
}
|
|
338
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
339
|
+
if (resolvedReadonly.value) return
|
|
340
|
+
if (!activeOption) return
|
|
341
|
+
event.preventDefault()
|
|
342
|
+
activeOption.activate()
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
if (event.key.length === 1 && !event.altKey && !event.ctrlKey && !event.metaKey) {
|
|
346
|
+
typeaheadBuffer += event.key.toLocaleLowerCase()
|
|
347
|
+
if (typeaheadTimer) clearTimeout(typeaheadTimer)
|
|
348
|
+
typeaheadTimer = setTimeout(() => { typeaheadBuffer = ''; typeaheadTimer = undefined }, 500)
|
|
349
|
+
const query = [...typeaheadBuffer].every((character) => character === typeaheadBuffer[0])
|
|
350
|
+
? typeaheadBuffer[0]!
|
|
351
|
+
: typeaheadBuffer
|
|
352
|
+
const currentIndex = activeOption === undefined ? -1 : options.indexOf(activeOption)
|
|
353
|
+
const searchOrder = currentIndex >= 0
|
|
354
|
+
? [...options.slice(currentIndex + 1), ...options.slice(0, currentIndex + 1)]
|
|
355
|
+
: options
|
|
356
|
+
const match = searchOrder.find((option) => option.label().toLocaleLowerCase().startsWith(query))
|
|
357
|
+
if (match) {
|
|
358
|
+
event.preventDefault()
|
|
359
|
+
focusOption(match, options)
|
|
360
|
+
}
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
if (!['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key) || options.length === 0) return
|
|
364
|
+
const currentIndex = activeOption === undefined ? -1 : options.indexOf(activeOption)
|
|
365
|
+
const nextIndex = event.key === 'Home'
|
|
366
|
+
? 0
|
|
367
|
+
: event.key === 'End'
|
|
368
|
+
? options.length - 1
|
|
369
|
+
: event.key === 'ArrowDown'
|
|
370
|
+
? (Math.max(currentIndex, -1) + 1) % options.length
|
|
371
|
+
: (currentIndex <= 0 ? options.length : currentIndex) - 1
|
|
372
|
+
const target = options[nextIndex]
|
|
373
|
+
if (!target) return
|
|
374
|
+
event.preventDefault()
|
|
375
|
+
focusOption(target, options)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function focusSelectedOrFirst(): void {
|
|
379
|
+
const options = enabledRegistrations()
|
|
380
|
+
const target = options.find((option) => option.selected()) ?? options[0]
|
|
381
|
+
if (target) focusOption(target, options)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
onBeforeUnmount(() => { if (typeaheadTimer) clearTimeout(typeaheadTimer) })
|
|
385
|
+
defineExpose({ focusSelectedOrFirst })
|
|
386
|
+
</script>
|
|
387
|
+
|
|
388
|
+
<style lang="postcss" src="./SListbox.css" scoped></style>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-listbox-option__control {
|
|
4
|
+
@apply relative flex w-full min-w-0 items-center justify-between rounded-md px-3 py-2 text-left text-sm text-surface-800;
|
|
5
|
+
@apply cursor-pointer hover:bg-surface-100;
|
|
6
|
+
@apply dark:text-surface-100 dark:hover:bg-surface-800;
|
|
7
|
+
transition: background-color var(--s-motion-standard);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.s-listbox-option__control:focus-visible {
|
|
11
|
+
outline: var(--s-focus-ring-width) solid var(--s-focus-ring-strong-color);
|
|
12
|
+
outline-offset: var(--s-focus-ring-inset-offset);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.s-listbox-option__control[aria-disabled='true'] {
|
|
16
|
+
@apply cursor-not-allowed opacity-50;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.s-listbox-option__control[aria-disabled='true']:hover {
|
|
20
|
+
@apply bg-transparent;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.s-listbox-option__control[aria-selected='true'] { @apply bg-primary-50 dark:bg-primary-950/40; }
|
|
24
|
+
.s-listbox-option__control[data-appearance='color'] {
|
|
25
|
+
width: var(--s-control-hitbox-lg);
|
|
26
|
+
height: var(--s-control-hitbox-lg);
|
|
27
|
+
@apply justify-center rounded-full p-1;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.s-listbox-option__main {
|
|
31
|
+
@apply flex min-w-0 items-center;
|
|
32
|
+
gap: var(--s-space-control-gap);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.s-listbox-option__label {
|
|
36
|
+
@apply block truncate;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.s-listbox-option__label[data-selected='true'] {
|
|
40
|
+
@apply font-semibold;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.s-listbox-option__check {
|
|
44
|
+
@apply absolute inset-y-0 right-0 flex items-center pr-4;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.s-listbox-option__check > svg {
|
|
48
|
+
width: var(--s-icon-size-sm);
|
|
49
|
+
height: var(--s-icon-size-sm);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.s-listbox-option__check[data-appearance='color'] {
|
|
53
|
+
@apply -right-0.5 -top-0.5 bottom-auto items-center justify-center rounded-full bg-surface-0 p-0 text-primary-600;
|
|
54
|
+
@apply dark:bg-surface-950 dark:text-primary-400;
|
|
55
|
+
width: var(--s-icon-size-sm);
|
|
56
|
+
height: var(--s-icon-size-sm);
|
|
57
|
+
box-shadow: var(--s-elevation-control);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.s-listbox-option__check[data-appearance='color'] > svg {
|
|
61
|
+
width: var(--s-icon-size-2xs);
|
|
62
|
+
height: var(--s-icon-size-2xs);
|
|
63
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<li
|
|
3
|
+
ref="optionRef"
|
|
4
|
+
class="s-listbox-option__control"
|
|
5
|
+
:data-appearance="appearance"
|
|
6
|
+
:data-focused="focused || undefined"
|
|
7
|
+
:title="title ?? label"
|
|
8
|
+
:aria-label="appearance === 'color' ? label : undefined"
|
|
9
|
+
role="option"
|
|
10
|
+
:aria-selected="selected"
|
|
11
|
+
:aria-disabled="disabled || undefined"
|
|
12
|
+
:tabindex="-1"
|
|
13
|
+
@focus="focused = true"
|
|
14
|
+
@blur="focused = false"
|
|
15
|
+
@click.stop="select"
|
|
16
|
+
>
|
|
17
|
+
<span class="s-listbox-option__main" :data-appearance="appearance">
|
|
18
|
+
<SMarker v-if="marker" :marker="marker" :size="appearance === 'color' ? 'lg' : 'sm'" />
|
|
19
|
+
<span v-if="appearance === 'select'" class="s-listbox-option__label" :data-selected="selected || undefined">
|
|
20
|
+
{{ label }}
|
|
21
|
+
</span>
|
|
22
|
+
</span>
|
|
23
|
+
<span v-if="selected" class="s-listbox-option__check" :data-appearance="appearance" aria-hidden="true">
|
|
24
|
+
<Check />
|
|
25
|
+
</span>
|
|
26
|
+
</li>
|
|
27
|
+
</template>
|
|
28
|
+
|
|
29
|
+
<script setup lang="ts">
|
|
30
|
+
import { inject, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
31
|
+
import { Check } from '../../icons/sputnigUiIcons'
|
|
32
|
+
import { useInteractiveLeafRegistration } from '../../../internal/passiveContentContract'
|
|
33
|
+
import SMarker from './SMarker.vue'
|
|
34
|
+
import { listboxContextKey } from '../_internal/listboxContext'
|
|
35
|
+
import type { SMarker as MarkerContract } from '../_internal/markerContract'
|
|
36
|
+
|
|
37
|
+
export interface Props {
|
|
38
|
+
registrationToken: string
|
|
39
|
+
label: string
|
|
40
|
+
selected: boolean
|
|
41
|
+
disabled?: boolean
|
|
42
|
+
appearance?: 'select' | 'color'
|
|
43
|
+
marker?: MarkerContract
|
|
44
|
+
title?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
defineOptions({ inheritAttrs: false })
|
|
48
|
+
|
|
49
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
50
|
+
disabled: false,
|
|
51
|
+
appearance: 'select',
|
|
52
|
+
marker: undefined,
|
|
53
|
+
title: undefined,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits<{
|
|
57
|
+
select: []
|
|
58
|
+
}>()
|
|
59
|
+
useInteractiveLeafRegistration({ owner: 'SListboxOption' })
|
|
60
|
+
const context = inject(listboxContextKey)
|
|
61
|
+
if (!context) throw new Error('SListboxOption must be used inside SListbox')
|
|
62
|
+
const focused = ref(false)
|
|
63
|
+
const optionRef = ref<HTMLLIElement | null>(null)
|
|
64
|
+
let unregister: (() => void) | undefined
|
|
65
|
+
const select = (): void => {
|
|
66
|
+
if (props.disabled) return
|
|
67
|
+
optionRef.value?.focus()
|
|
68
|
+
emit('select')
|
|
69
|
+
}
|
|
70
|
+
onMounted(() => {
|
|
71
|
+
const element = optionRef.value
|
|
72
|
+
if (!(element instanceof HTMLLIElement)) throw new Error('SListboxOption: option element is unavailable')
|
|
73
|
+
unregister = context.register({
|
|
74
|
+
element,
|
|
75
|
+
token: props.registrationToken,
|
|
76
|
+
activate: select,
|
|
77
|
+
disabled: () => props.disabled,
|
|
78
|
+
label: () => props.label,
|
|
79
|
+
selected: () => props.selected,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
onBeforeUnmount(() => unregister?.())
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<style lang="postcss" src="./SListboxOption.css" scoped></style>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
.s-marker { @apply inline-block h-2 w-2 shrink-0 rounded-full bg-surface-400 ring-1 ring-inset ring-surface-500/50; }
|
|
3
|
+
.s-marker[data-size='lg'] { @apply h-7 w-7 ring-2 ring-white/80 shadow-sm dark:ring-surface-950/80; }
|
|
4
|
+
.s-marker[data-tone='primary'] { @apply bg-primary-500 ring-primary-600/50; }
|
|
5
|
+
.s-marker[data-tone='success'] { @apply bg-success-500 ring-success-600/50; }
|
|
6
|
+
.s-marker[data-tone='blue'] { @apply bg-blue-400 ring-blue-500/50; }
|
|
7
|
+
.s-marker[data-tone='sky'] { @apply bg-sky-400 ring-sky-500/50; }
|
|
8
|
+
.s-marker[data-tone='indigo'] { @apply bg-indigo-400 ring-indigo-500/50; }
|
|
9
|
+
.s-marker[data-tone='emerald'] { @apply bg-emerald-400 ring-emerald-500/50; }
|
|
10
|
+
.s-marker[data-tone='green'] { @apply bg-green-400 ring-green-500/50; }
|
|
11
|
+
.s-marker[data-tone='teal'] { @apply bg-teal-400 ring-teal-500/50; }
|
|
12
|
+
.s-marker[data-tone='amber'] { @apply bg-amber-400 ring-amber-500/50; }
|
|
13
|
+
.s-marker[data-tone='orange'] { @apply bg-orange-400 ring-orange-500/50; }
|
|
14
|
+
.s-marker[data-tone='rose'] { @apply bg-rose-400 ring-rose-500/50; }
|
|
15
|
+
.s-marker[data-tone='pink'] { @apply bg-pink-400 ring-pink-500/50; }
|
|
16
|
+
.s-marker[data-tone='fuchsia'] { @apply bg-fuchsia-400 ring-fuchsia-500/50; }
|
|
17
|
+
.s-marker[data-tone='cyan'] { @apply bg-cyan-400 ring-cyan-500/50; }
|
|
18
|
+
.s-marker[data-tone='lime'] { @apply bg-lime-400 ring-lime-500/50; }
|
|
19
|
+
.s-marker[data-tone='custom'] { background-color: var(--s-marker-custom-color); --tw-ring-color: var(--s-marker-custom-color); }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<span
|
|
3
|
+
v-bind="ownedAttrs.bindings()"
|
|
4
|
+
class="s-marker"
|
|
5
|
+
:data-tone="marker.tone"
|
|
6
|
+
:data-size="size"
|
|
7
|
+
:style="customProperties"
|
|
8
|
+
aria-hidden="true"
|
|
9
|
+
/>
|
|
10
|
+
</template>
|
|
11
|
+
|
|
12
|
+
<script lang="ts">
|
|
13
|
+
export { createCustomMarker } from '../_internal/markerContract'
|
|
14
|
+
export type { SCustomMarker, SMarker, SMarkerTone } from '../_internal/markerContract'
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<script setup lang="ts">
|
|
18
|
+
import { computed } from 'vue'
|
|
19
|
+
import { useOwnedAttrs } from '../../../internal/ownedAttrs'
|
|
20
|
+
import { createCustomMarker, type SMarker } from '../_internal/markerContract'
|
|
21
|
+
|
|
22
|
+
defineOptions({ inheritAttrs: false })
|
|
23
|
+
|
|
24
|
+
const props = withDefaults(defineProps<{ marker: SMarker; size?: 'sm' | 'lg' }>(), { size: 'sm' })
|
|
25
|
+
const ownedAttrs = useOwnedAttrs({ component: 'SMarker', owner: 'marker root' })
|
|
26
|
+
const customProperties = computed(() => {
|
|
27
|
+
if (props.marker.tone !== 'custom') return undefined
|
|
28
|
+
createCustomMarker(props.marker.color)
|
|
29
|
+
return { '--s-marker-custom-color': props.marker.color }
|
|
30
|
+
})
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<style lang="postcss" src="./SMarker.css" scoped></style>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
@reference "../../../styles/reference.css";
|
|
2
|
+
|
|
3
|
+
.s-menu-surface {
|
|
4
|
+
@apply m-0 rounded-xl p-1.5;
|
|
5
|
+
@apply focus:outline-none;
|
|
6
|
+
background: var(--s-floating-surface);
|
|
7
|
+
color: var(--s-floating-foreground);
|
|
8
|
+
box-shadow: 0 0 0 1px var(--s-floating-outline), var(--s-elevation-floating);
|
|
9
|
+
width: var(--s-menu-surface-width);
|
|
10
|
+
min-width: var(--s-menu-surface-min-width);
|
|
11
|
+
max-width: var(--s-menu-surface-max-width);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.s-menu-surface[data-density='compact'] {
|
|
15
|
+
@apply min-w-40 rounded-md p-1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.s-menu-surface[data-orientation='horizontal'] {
|
|
19
|
+
@apply flex max-w-full gap-1 overflow-x-auto;
|
|
20
|
+
}
|