feat(ui): [WIP] Layout section component

This commit is contained in:
upsiflu 2025-01-03 13:42:39 +01:00
parent 4a62f05d1e
commit 860f12512c
13 changed files with 1007 additions and 27 deletions

View File

@ -158,6 +158,16 @@ const sortedFiles = computed(() => {
const hasActiveUploads = computed(() => files.value.some(file => file.active))
// const isOpen = computed({
// get() {
// return store.state.ui.modalsOpen.has(modalName);
// },
// set(value) {
// store.commit('ui/setModal', [modalName, value]);
// }
// })
//
// Quota status
//

View File

@ -1,16 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { RouterLinkProps } from 'vue-router';
import { type ColorProps, type DefaultProps, type PastelProps, type RaisedProps, type VariantProps, color } from '~/composables/color'
import { type WidthProps, width } from '~/composables/width'
const props = defineProps<{
columnWidth?: string,
noGap?: true,
noRule?: true,
noWrap?: true
}
& { [P in "stack" | "grid" | "flex" | "columns" | "row" | "page"]?: true | string }
noWrap?: true,
} & { [P in "stack" | "grid" | "flex" | "columns" | "row" | "page"]?: true | string }
& { [C in "nav" | "aside" | "header" | "footer" | "main" | "label" | "form" | "h1" | "h2" | "h3" | "h4" | "h5"]?: true }
& { [G in 'no-gap' | 'gap-16' | 'gap-12' | 'gap-8' | 'gap-auto' ]?: true }
& (PastelProps | ColorProps | DefaultProps)
& RaisedProps
& VariantProps
@ -18,6 +18,10 @@ const props = defineProps<{
const columnWidth = props.columnWidth ?? '46px'
const maybeGap = Object.entries(props).find(
([key, value]) => value===true && key.startsWith('gap'))
const gapWidth = maybeGap ? `${maybeGap[0].replace('gap', '')}px` : '32px'
const attributes = computed(() => ({
...color(props)(width(props)()),
layout:
@ -27,6 +31,8 @@ const attributes = computed(() => ({
props.columns ? 'columns' :
'stack'
}))
console.log("GRID", props.grid, props.grid ? 'grid-custom' : 'none')
</script>
<template>
@ -34,7 +40,7 @@ const attributes = computed(() => ({
:is="props.nav ? 'nav' : props.aside ? 'aside' : props.header ? 'header' : props.footer ? 'footer' : props.main ? 'main' : props.label ? 'label' : props.form ? 'form' : props.h1 ? 'h1' : props.h2 ? 'h2' : props.h3 ? 'h3' : props.h4 ? 'h4' : props.h5 ? 'h5' : 'div'"
:class="[
$style.layout,
noGap || $style.gap,
('noGap' in props && props.noGap === true) || $style.gap,
noWrap || $style.wrap,
]" v-bind="attributes">
<slot />
@ -47,7 +53,7 @@ const attributes = computed(() => ({
/* Override --gap with your preferred value */
gap: var(--gap, 32px);
gap: var(--gap, v-bind(gapWidth));
&:not(.gap) {
gap: 0;
}
@ -80,7 +86,7 @@ const attributes = computed(() => ({
&[layout=grid-custom] {
display: grid;
grid: v-bind(grid);
grid: v-bind("props.grid");
grid-auto-flow: row dense;
place-content: center;
/* If the grid has a fixed size smaller than its container, center it */

View File

@ -0,0 +1,130 @@
<script setup lang="ts">
import { computed } from 'vue';
import type { RouterLinkProps } from 'vue-router';
import Card from '~/components/ui/Card.vue'
import Layout from '~/components/ui/Layout.vue'
import Toggle from '~/components/ui/Toggle.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Activity from '~/components/ui/Activity.vue'
const props = defineProps<{
[M in 'tiny-items' | 'small-items' | 'medium-items']?: true }
& { [H in 'h1' | 'h2' | 'h3']?: string }
& { action?: { text: string } & (RouterLinkProps | { onClick: (...args: any[]) => void | Promise<void> }) }>()
const [headingLevel, title] =
props.h1 ? ['h1', props.h1]
: props.h2 ? ['h2', props.h2]
: ['h3', props.h3]
const headerGrid =
`auto / repeat(auto-fit, calc(46px * ${props['tiny-items'] ? 2 : props['small-items'] ? 3 : 4} + 32px * 2))`
console.log(headerGrid);
</script>
<template>
<section>
<Layout header :grid="headerGrid">
<!-- The title row's width is a multiple of the expected items' column span -->
<Layout flex no-gap
style="grid-column: 1 / -1; align-self: baseline;"
>
<!-- Set distance between baseline and previous row -->
<Spacer v
:size="64"
style="outline:1px solid red; align-self: baseline;"
/>
<!-- Flexible row content -->
<!-- Note that the `h3` uses its padding to create the 24px bottom gap -->
<component :is="headingLevel" style="align-self: baseline; padding:0 0 24px 10px; margin:0;">
{{ title }}
</component>
<Spacer grow />
<!-- Action! You can either specify `to` or `onClick`. -->
<Button v-if="props.action && 'onClick' in props.action"
ghost thin auto align-self="baseline"
style="grid-column:-1;"
:onClick="props.action.onClick"
>
{{ props.action.text }}
</Button>
<Link v-if="props.action && 'to' in props.action"
ghost thin auto align-self="baseline"
:to="props.action.to"
>
{{ props.action.text }}
</Link>
</Layout>
</Layout>
<Layout main
grid="auto / repeat(auto-fit, 46px)"
>
<slot />
</Layout>
</section>
</template>
<style module>
.layout {
transition: all .15s;
/* Override --gap with your preferred value */
gap: var(--gap, v-bind(gapWidth));
&:not(.gap) {
gap: 0;
}
/* Growth */
&:has(:global(>.grow)) {
>:not(:global(.grow)) {
flex-grow: 0;
}
}
/* Layout strategy */
&[layout=columns] {
column-count: auto;
column-width: v-bind(columnWidth);
display: block;
column-rule: 1px solid v-bind("noRule ? 'transparent' : 'var(--border-color)'");
}
&[layout=grid] {
display: grid;
grid-template-columns:
repeat(auto-fit, v-bind(columnWidth));
grid-auto-flow: row dense;
place-content: center;
/* If the grid has a fixed size smaller than its container, center it */
}
&[layout=grid-custom] {
display: grid;
grid: v-bind(grid);
grid-auto-flow: row dense;
place-content: center;
/* If the grid has a fixed size smaller than its container, center it */
}
&[layout=stack] {
display: flex;
flex-direction: column;
}
&[layout=flex] {
display: flex;
flex-direction: row;
flex-wrap: v-bind('props.noWrap ? "nowrap" : "wrap"');
}
}
</style>

View File

@ -26,11 +26,13 @@ const styles = {
const getStyle = (props: Partial<WidthProps>) => (key: Key): string =>
// @ts-ignore
typeof styles[key] === 'function' && key in props ?
// @ts-ignore
styles[key](
// TODO: Make the typescript compiler understand `key in props`
// @ts-ignore
props[key]
)
: styles[key]
// All keys are exclusive

View File

@ -5,6 +5,7 @@ import { color } from '~/composables/color';
import Sidebar from '~/ui/components/Sidebar.vue'
import ShortcutsModal from './modals/Shortcuts.vue'
import LanguagesModal from './modals/Languages.vue'
import UploadModal from './modals/Upload.vue';
// Fake content
onMounted(async () => {
@ -17,9 +18,10 @@ onMounted(async () => {
<template>
<div class="funkwhale grid">
<Sidebar/>
<RouterView v-bind="color({}, ['default', 'solid'])()" />
<RouterView v-bind="color({}, ['default', 'solid'])" />
<LanguagesModal />
<ShortcutsModal />
<UploadModal />
</div>
</template>

View File

@ -35,8 +35,8 @@ const logoUrl = computed(() => store.state.auth.authenticated ? 'library.index'
</script>
<template>
<Layout aside :class="[$style.sidebar, $style['sticky-content']]" default solid raised>
<Layout flex no-gap header style="justify-content:space-between; padding-right:8px;">
<Layout aside default solid raised gap-12 :class="[$style.sidebar, $style['sticky-content']]">
<Layout header flex no-gap style="justify-content:space-between; padding-right:8px;">
<Link
:to="{name: logoUrl}"
:class="$style['logo']"
@ -55,10 +55,13 @@ const logoUrl = computed(() => store.state.auth.authenticated ? 'library.index'
>
</Link>
<Link to="/upload"
<Button
align-self="center"
round icon="bi-upload"
class="is-icon-only"
ghost
@click="store.commit('ui/toggleModal', 'upload')"
:aria-pressed="store.state.ui.modalsOpen.has('languages') || undefined"
>
<Transition>
<div
@ -72,7 +75,7 @@ const logoUrl = computed(() => store.state.auth.authenticated ? 'library.index'
/>
</div>
</Transition>
</Link>
</Button>
<UserMenu/>

View File

@ -3,10 +3,13 @@ import { computed, ref, reactive } from 'vue'
import { useUploadsStore } from '~/ui/stores/upload'
import { bytesToHumanSize } from '~/ui/composables/bytes'
import { useRouter } from 'vue-router'
import { useStore } from '~/store'
import UploadList from '~/ui/components/UploadList.vue'
import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
import Modal from '~/components/ui/Modal.vue'
import Input from '~/components/ui/Input.vue'
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
const uploads = useUploadsStore()
@ -67,23 +70,35 @@ const sortItems = reactive([
])
const currentSort = ref(sortItems[0])
const store = useStore()
// Filtering
const filterItems = reactive([
{ label: 'All', value: 'all' }
])
const currentFilter = ref(filterItems[0])
const modalName = 'upload'
const isOpen = computed({
get() {
return store.state.ui.modalsOpen.has(modalName);
},
set(value) {
store.commit('ui/setModal', [modalName, value]);
}
})
</script>
<template>
<Modal
v-model="libraryOpen"
title="Upload music to library"
v-model="isOpen"
title="Upload..."
>
<template #alert="closeAlert">
<Alert yellow>
Before uploading, please ensure your files are tagged properly.
We recommend using Picard for that purpose.
<template #actions>
<Button @click="closeAlert">
Got it
@ -92,8 +107,9 @@ const currentFilter = ref(filterItems[0])
</Alert>
</template>
<FwFileInput
:accept="['.flac', '.ogg', '.opus', '.mp3', '.aac', '.aif', '.aiff', '.m4a']"
<Input
type="file"
:accept="['.flac', '.ogg', '.opus', '.mp3', '.aac', '.aif', '.aiff', '.m4a'].join(', ')"
multiple
auto-reset
@files="processFiles"

View File

@ -0,0 +1,176 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import onKeyboardShortcut from '~/composables/onKeyboardShortcut';
import Modal from '~/components/ui/Modal.vue'
import Button from '~/components/ui/Button.vue'
import Input from '~/components/ui/Input.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Alert from '~/components/ui/Alert.vue';
import Card from '~/components/ui/Card.vue';
import Pagination from '~/components/ui/Pagination.vue';
import type { Actor, Channel } from '~/types';
import FileUploadWidget from '~/components/library/FileUploadWidget.vue';
import type { VueUploadItem } from 'vue-upload-component';
import UploadForm from '~/components/channels/UploadForm.vue';
import FileUpload from '~/components/library/FileUpload.vue';
const { t } = useI18n()
const store = useStore()
const modalName = 'upload'
const isOpen = computed({
get() {
return store.state.ui.modalsOpen.has(modalName);
},
set(value) {
store.commit('ui/setModal', [modalName, value]);
}
})
onKeyboardShortcut('u', () => store.commit('ui/toggleModal', modalName))
const dummyActor = (number:number) : Actor => ({
id: number,
summary: "actor summary",
preferred_username: `actor ${number}`,
full_username: "actor full username",
is_local: false,
domain: "domain"
})
const dummyChannel=(number:number) : Channel=>
({
id : number,
uuid : 'uuid',
actor: dummyActor(number),
attributed_to: dummyActor(number),
rss_url: "rss url string",
subscriptions_count: 10,
downloads_count: 10,
content_category: 'music'
})
type UploadDestination = 'channel' | 'library' | 'podcast'
type State =
{ uploadDestination? : UploadDestination, page: typeof pages[number], files?: string[] }
// initial state
const state = ref<State>({page : 'selectDestination'});
const pages = ['selectDestination', 'uploadFiles', 'uploadsInProgress'] as const
// Step 1
const destinationSelected = (destination: UploadDestination) =>
state.value = {...state.value, uploadDestination:destination, page:'uploadFiles' }
// Step 2
const filesSelected = (e: InputEvent)=>{
state.value = {...state.value, files: [] }
}
const modalTitle = computed(()=>
({ 'selectDestination' : 'Upload', 'uploadFiles' : 'Select files for upload', 'uploadsInProgress': 'Uploading...'}
[state.value.page])
)
// Upload input metadata
const values = reactive({
channel: null,
license: null,
album: null
})
const channels = [0,1,2,3,4].map(dummyChannel)
</script>
<template>
<Modal overPopover
:title="modalTitle"
v-model="isOpen"
>
<template #alert v-if="state.page === 'selectDestination'">
<Alert yellow>
Before uploading, please ensure your files are tagged properly.
We recommend using Picard for that purpose.
</Alert>
</template>
<Layout flex style="place-content:center" v-if="state.page === 'selectDestination'">
<Card small solid secondary title="Music"
@click="destinationSelected('channel')"
icon="bi-upload"
>
<template #image>
<i class="bi bi-music-note-beamed solid primary" :class="$style.icon"></i>
</template>
Publish music you make
</Card>
<Card small solid secondary title="Podcast"
@click="destinationSelected('podcast')"
icon="bi-upload"
>
<template #image>
<i class="bi bi-mic-fill solid primary" :class="$style.icon"></i>
</template>
Publish podcasts you make
</Card>
<Card small solid secondary title="Mix & Share"
@click="destinationSelected('library')"
icon="bi-upload"
>
<template #image>
<i class="bi bi-headphones solid secondary raised" :class="$style.icon"></i>
</template>
Host music you listen to
</Card>
</Layout>
<Layout stack v-if="state.page === 'uploadFiles'">
UPLOAD FILES
<!-- <Input
type="file"
:accept="['.flac', '.ogg', '.opus', '.mp3', '.aac', '.aif', '.aiff', '.m4a'].join(', ')"
multiple
auto-reset
@input="filesSelected"
/> -->
<!-- <UploadForm></UploadForm> -->
<FileUpload :library="{ uuid: 'string'}"></FileUpload>
{{ state.files }}
</Layout>
<template #actions>
<Button secondary
>
Cancel
</Button>
<Button disabled v-if="state.page === 'selectDestination'">
Continue in background' : 'Save and close'
</Button>
<Button primary v-if="state.page === 'uploadFiles'">
Continue in background' : 'Save and close'
</Button>
</template>
</Modal>
</template>
<style module>
.icon {
font-size:100px;
padding:28px;
inset:0;
display:block;
}
</style>

View File

@ -49,6 +49,7 @@ export default defineConfig({
text: 'Layout', link: '/components/ui/layout/',
items: [
{ text: "Spacer", link: "/components/ui/layout/spacer" },
{ text: "Section", link: "/components/ui/layout/section" },
{ text: "Using `flex`", link: "/components/ui/layout/flex" },
{ text: "Using `stack`", link: "/components/ui/layout/stack" },
{ text: "Using `grid`", link: "/components/ui/layout/grid" },

View File

@ -0,0 +1,227 @@
<script setup lang="ts">
import { computed, ref } from 'vue'
import { type Track, type User } from '~/types'
import Card from '~/components/ui/Card.vue'
import Layout from '~/components/ui/Layout.vue'
import Toggle from '~/components/ui/Toggle.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Button from '~/components/ui/Button.vue'
import Activity from '~/components/ui/Activity.vue'
import Section from '~/components/ui/layout/Section.vue'
const alignLeft = ref(false)
const attributes = computed(() => ({
style: alignLeft.value ? 'justify-content: start' : ''
}))
const track: Track = {
id: 0,
fid: "",
title: 'Some lovely track',
description: {
content_type: 'text/markdown',
text: `**New:** Music for the eyes!`
},
cover: {
uuid: "",
urls: {
original: 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb',
medium_square_crop: 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb',
large_square_crop: 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'
}
},
tags: ["example"],
uploads: [],
downloads_count: 1927549377,
artist_credit: [{
artist: {
id: 0,
fid: "",
name: "The Artist",
description: {
content_type: 'text/markdown',
text: `I'm a musician based on the internet.
Find all my music on [Funkwhale](https://funkwhale.audio)!`},
tags: [],
content_category: 'music',
albums: [],
tracks_count: 1,
attributed_to: {
id: 0,
summary: "",
preferred_username: "User12345",
full_username: "User12345",
is_local: false,
domain: "myDomain.io"
},
is_local: false,
is_playable: true
},
credit: "",
joinphrase: " and ",
index: 22
}],
disc_number: 7,
listen_url: "https://funkwhale.audio",
creation_date: "12345",
attributed_to: {
id: 0,
summary: "",
preferred_username: "User12345",
full_username: "User12345",
is_local: false,
domain: "myDomain.io"
},
is_playable: true,
is_local: false
}
const user: User = {
id: 12,
avatar: {
uuid: "",
urls: {
original: 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb',
medium_square_crop: 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb',
large_square_crop: 'https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb'
}
},
email: "user12345@example.org",
summary: { text: "Hi! I'm Example from The Internet.", content_type: "text" },
username: "user12345",
full_username: "user12345",
instance_support_message_display_date: "?",
funkwhale_support_message_display_date: "?",
is_superuser: true,
privacy_level: "everyone"
}
</script>
# Layout section
<Layout flex>
<Toggle v-model="alignLeft" label="Left-align the layout"/>
</Layout>
---
<div class="preview" style="margin: 0 -40px; padding: 0 25px;">
<Section small-items h1="Hello" :action="{ text:'more...', to:'/' }">
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
</Section>
<Layout
grid="auto / repeat(auto-fit, calc(46px _ 3 + 32px _ 2))"
v-bind="attributes"
>
<!-- The title row's width is a multiple of 3 rows -->
<Layout flex no-gap
style="grid-column: 1 / -1; align-self: baseline;"
>
<!-- Set distance between baseline and previous row -->
<Spacer v
:size="64"
style="outline:1px solid red; align-self: baseline;"
/>
<!-- Flexible row content -->
<!-- Note that the `h3` uses its padding to create the 24px bottom gap -->
<h3 style="align-self: baseline; padding:0 0 24px 10px; margin:0;">
Albums
</h3>
<Spacer grow />
<Button ghost thin auto align-self="baseline"
style="grid-column:-1;"
>
Show all
</Button>
</Layout>
</Layout>
<Layout solid default
style="position:relative;"
grid="auto / repeat(auto-fit, 46px)"
v-bind="attributes"
>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
<Card small title="Relatively Long Album Name">
Artist Name
13 tracks
</Card>
</Layout>
<Layout
grid="auto / repeat(auto-fit, calc(46px _ 4 + 32px _ 3))"
v-bind="attributes"
>
<Layout flex no-gap
style="grid-column: 1 / -1; align-self: baseline;"
>
<!-- Set distance between baseline and previous row -->
<Spacer v
:size="64"
style="outline:1px solid red; align-self: baseline;"
/>
<!-- Flexible row content -->
<!-- Note that the `h3` uses its padding to create the 24px bottom gap -->
<h3 style="align-self: baseline; padding:0 0 24px 10px; margin:0;">
Tracks
</h3>
<Spacer grow />
<Button ghost thin auto align-self="baseline"
style="grid-column:-1;"
>
Show all
</Button>
</Layout>
</Layout>
<Layout solid default
style="position:relative;"
grid="auto / repeat(auto-fit, 46px)"
v-bind="attributes"
>
<Activity :track="track" :user="user" />
<Activity :track="track" :user="user" />
<Activity :track="track" :user="user" />
</Layout>
</div>

View File

@ -14,23 +14,21 @@ You can align items inside `flex` and `grid` layouts.
<template
v-for="alignment in ['start', 'center', 'end', 'auto', 'baseline', 'stretch']"
:key="alignment"
:key="alignment">
>
<div style="position:relative;place-self:stretch; grid-area: span 2 / span 2; min-height: 72px; border:.5px solid red; display:grid; grid: 1fr / 1fr; grid-auto-flow: column;"
>
<Button auto primary :align-self="alignment">🐌</Button>
<div style="position:relative;place-self:stretch; grid-area: span 2 / span 2; min-height: 72px; border:.5px solid red; display:grid; grid: 1fr / 1fr; grid-auto-flow: column;"
>
<Button auto primary :align-self="alignment">🐌</Button>
_
<span style="position:absolute; right: 0; margin-left:-20px; bottom:0;">align-self={{ alignment }}</span>
</div>
</div>
</template>
<template
v-for="alignment in ['center', 'stretch']"
:key="alignment"
>
:key="alignment">
<div style="position:relative;place-self:stretch; grid-area: span 2 / span 2; min-height: 72px; border:.5px solid red; display:grid; grid: 1fr / 1fr; grid-auto-flow: column;"
>

6
package.json Normal file
View File

@ -0,0 +1,6 @@
{
"devDependencies": {
"openapi-typescript": "^7.4.4",
"typescript": "^5.7.2"
}
}

403
pnpm-lock.yaml Normal file
View File

@ -0,0 +1,403 @@
lockfileVersion: "9.0"
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
openapi-typescript:
specifier: ^7.4.4
version: 7.4.4(typescript@5.7.2)
typescript:
specifier: ^5.7.2
version: 5.7.2
packages:
"@babel/code-frame@7.26.2":
resolution:
{
integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==,
}
engines: { node: ">=6.9.0" }
"@babel/helper-validator-identifier@7.25.9":
resolution:
{
integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==,
}
engines: { node: ">=6.9.0" }
"@redocly/ajv@8.11.2":
resolution:
{
integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==,
}
"@redocly/config@0.17.1":
resolution:
{
integrity: sha512-CEmvaJuG7pm2ylQg53emPmtgm4nW2nxBgwXzbVEHpGas/lGnMyN8Zlkgiz6rPw0unASg6VW3wlz27SOL5XFHYQ==,
}
"@redocly/openapi-core@1.26.1":
resolution:
{
integrity: sha512-xRuVZqMVRFzqjbUCpOTra4tbnmQMWsya996omZMV3WgD084Z6OWB3FXflhAp93E/yAmbWlWZpddw758AyoaLSw==,
}
engines: { node: ">=14.19.0", npm: ">=7.0.0" }
agent-base@7.1.3:
resolution:
{
integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==,
}
engines: { node: ">= 14" }
ansi-colors@4.1.3:
resolution:
{
integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==,
}
engines: { node: ">=6" }
argparse@2.0.1:
resolution:
{
integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==,
}
balanced-match@1.0.2:
resolution:
{
integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==,
}
brace-expansion@2.0.1:
resolution:
{
integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==,
}
change-case@5.4.4:
resolution:
{
integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==,
}
colorette@1.4.0:
resolution:
{
integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==,
}
debug@4.4.0:
resolution:
{
integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==,
}
engines: { node: ">=6.0" }
peerDependencies:
supports-color: "*"
peerDependenciesMeta:
supports-color:
optional: true
fast-deep-equal@3.1.3:
resolution:
{
integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==,
}
https-proxy-agent@7.0.6:
resolution:
{
integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==,
}
engines: { node: ">= 14" }
index-to-position@0.1.2:
resolution:
{
integrity: sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==,
}
engines: { node: ">=18" }
js-levenshtein@1.1.6:
resolution:
{
integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==,
}
engines: { node: ">=0.10.0" }
js-tokens@4.0.0:
resolution:
{
integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==,
}
js-yaml@4.1.0:
resolution:
{
integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==,
}
hasBin: true
json-schema-traverse@1.0.0:
resolution:
{
integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==,
}
minimatch@5.1.6:
resolution:
{
integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==,
}
engines: { node: ">=10" }
ms@2.1.3:
resolution:
{
integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==,
}
node-fetch@2.7.0:
resolution:
{
integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==,
}
engines: { node: 4.x || >=6.0.0 }
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
openapi-typescript@7.4.4:
resolution:
{
integrity: sha512-7j3nktnRzlQdlHnHsrcr6Gqz8f80/RhfA2I8s1clPI+jkY0hLNmnYVKBfuUEli5EEgK1B6M+ibdS5REasPlsUw==,
}
hasBin: true
peerDependencies:
typescript: ^5.x
parse-json@8.1.0:
resolution:
{
integrity: sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==,
}
engines: { node: ">=18" }
picocolors@1.1.1:
resolution:
{
integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==,
}
pluralize@8.0.0:
resolution:
{
integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==,
}
engines: { node: ">=4" }
require-from-string@2.0.2:
resolution:
{
integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==,
}
engines: { node: ">=0.10.0" }
supports-color@9.4.0:
resolution:
{
integrity: sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==,
}
engines: { node: ">=12" }
tr46@0.0.3:
resolution:
{
integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==,
}
type-fest@4.31.0:
resolution:
{
integrity: sha512-yCxltHW07Nkhv/1F6wWBr8kz+5BGMfP+RbRSYFnegVb0qV/UMT0G0ElBloPVerqn4M2ZV80Ir1FtCcYv1cT6vQ==,
}
engines: { node: ">=16" }
typescript@5.7.2:
resolution:
{
integrity: sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==,
}
engines: { node: ">=14.17" }
hasBin: true
uri-js-replace@1.0.1:
resolution:
{
integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==,
}
webidl-conversions@3.0.1:
resolution:
{
integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==,
}
whatwg-url@5.0.0:
resolution:
{
integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==,
}
yaml-ast-parser@0.0.43:
resolution:
{
integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==,
}
yargs-parser@21.1.1:
resolution:
{
integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==,
}
engines: { node: ">=12" }
snapshots:
"@babel/code-frame@7.26.2":
dependencies:
"@babel/helper-validator-identifier": 7.25.9
js-tokens: 4.0.0
picocolors: 1.1.1
"@babel/helper-validator-identifier@7.25.9": {}
"@redocly/ajv@8.11.2":
dependencies:
fast-deep-equal: 3.1.3
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
uri-js-replace: 1.0.1
"@redocly/config@0.17.1": {}
"@redocly/openapi-core@1.26.1(supports-color@9.4.0)":
dependencies:
"@redocly/ajv": 8.11.2
"@redocly/config": 0.17.1
colorette: 1.4.0
https-proxy-agent: 7.0.6(supports-color@9.4.0)
js-levenshtein: 1.1.6
js-yaml: 4.1.0
minimatch: 5.1.6
node-fetch: 2.7.0
pluralize: 8.0.0
yaml-ast-parser: 0.0.43
transitivePeerDependencies:
- encoding
- supports-color
agent-base@7.1.3: {}
ansi-colors@4.1.3: {}
argparse@2.0.1: {}
balanced-match@1.0.2: {}
brace-expansion@2.0.1:
dependencies:
balanced-match: 1.0.2
change-case@5.4.4: {}
colorette@1.4.0: {}
debug@4.4.0(supports-color@9.4.0):
dependencies:
ms: 2.1.3
optionalDependencies:
supports-color: 9.4.0
fast-deep-equal@3.1.3: {}
https-proxy-agent@7.0.6(supports-color@9.4.0):
dependencies:
agent-base: 7.1.3
debug: 4.4.0(supports-color@9.4.0)
transitivePeerDependencies:
- supports-color
index-to-position@0.1.2: {}
js-levenshtein@1.1.6: {}
js-tokens@4.0.0: {}
js-yaml@4.1.0:
dependencies:
argparse: 2.0.1
json-schema-traverse@1.0.0: {}
minimatch@5.1.6:
dependencies:
brace-expansion: 2.0.1
ms@2.1.3: {}
node-fetch@2.7.0:
dependencies:
whatwg-url: 5.0.0
openapi-typescript@7.4.4(typescript@5.7.2):
dependencies:
"@redocly/openapi-core": 1.26.1(supports-color@9.4.0)
ansi-colors: 4.1.3
change-case: 5.4.4
parse-json: 8.1.0
supports-color: 9.4.0
typescript: 5.7.2
yargs-parser: 21.1.1
transitivePeerDependencies:
- encoding
parse-json@8.1.0:
dependencies:
"@babel/code-frame": 7.26.2
index-to-position: 0.1.2
type-fest: 4.31.0
picocolors@1.1.1: {}
pluralize@8.0.0: {}
require-from-string@2.0.2: {}
supports-color@9.4.0: {}
tr46@0.0.3: {}
type-fest@4.31.0: {}
typescript@5.7.2: {}
uri-js-replace@1.0.1: {}
webidl-conversions@3.0.1: {}
whatwg-url@5.0.0:
dependencies:
tr46: 0.0.3
webidl-conversions: 3.0.1
yaml-ast-parser@0.0.43: {}
yargs-parser@21.1.1: {}