Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 20 additions & 25 deletions app/components/Filter/Panel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -243,17 +243,16 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
role="radiogroup"
:aria-label="$t('filters.weekly_downloads')"
>
<TagClickable
<TagRadioButton
v-for="range in DOWNLOAD_RANGES"
:key="range.value"
type="button"
role="radio"
:aria-checked="filters.downloadRange === range.value"
:status="filters.downloadRange === range.value ? 'active' : 'default'"
@click="emit('update:downloadRange', range.value)"
:model-value="filters.downloadRange"
:value="range.value"
@update:modelValue="emit('update:downloadRange', $event as DownloadRange)"
name="range"
>
{{ $t(getDownloadRangeLabelKey(range.value)) }}
</TagClickable>
</TagRadioButton>
</div>
</fieldset>

Expand All @@ -267,17 +266,16 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
role="radiogroup"
:aria-label="$t('filters.updated_within')"
>
<TagClickable
<TagRadioButton
v-for="option in UPDATED_WITHIN_OPTIONS"
:key="option.value"
type="button"
role="radio"
:aria-checked="filters.updatedWithin === option.value"
:status="filters.updatedWithin === option.value ? 'active' : 'default'"
@click="emit('update:updatedWithin', option.value)"
:model-value="filters.updatedWithin"
:value="option.value"
name="updatedWithin"
@update:modelValue="emit('update:updatedWithin', $event as UpdatedWithin)"
>
{{ $t(getUpdatedWithinLabelKey(option.value)) }}
</TagClickable>
</TagRadioButton>
</div>
</fieldset>

Expand All @@ -290,17 +288,16 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
</span>
</legend>
<div class="flex flex-wrap gap-2" role="radiogroup" :aria-label="$t('filters.security')">
<TagClickable
<TagRadioButton
v-for="security in SECURITY_FILTER_VALUES"
:key="security"
type="button"
role="radio"
disabled
:aria-checked="filters.security === security"
:status="filters.security === security ? 'active' : 'default'"
:model-value="filters.security"
:value="security"
name="security"
>
{{ $t(getSecurityLabelKey(security)) }}
</TagClickable>
</TagRadioButton>
</div>
</fieldset>

Expand All @@ -310,16 +307,14 @@ const hasActiveFilters = computed(() => !!filterSummary.value)
{{ $t('filters.keywords') }}
</legend>
<div class="flex flex-wrap gap-1.5" role="group" :aria-label="$t('filters.keywords')">
<TagClickable
<TagButton
v-for="keyword in displayedKeywords"
:key="keyword"
type="button"
:aria-pressed="filters.keywords.includes(keyword)"
:status="filters.keywords.includes(keyword) ? 'active' : 'default'"
:pressed="filters.keywords.includes(keyword)"
@click="emit('toggleKeyword', keyword)"
>
{{ keyword }}
</TagClickable>
</TagButton>
<button
v-if="hasMoreKeywords"
type="button"
Expand Down
9 changes: 4 additions & 5 deletions app/components/Package/Card.vue
Original file line number Diff line number Diff line change
Expand Up @@ -162,17 +162,16 @@ const pkgDescription = useMarkdown(() => ({
:aria-label="$t('package.card.keywords')"
class="relative z-10 flex flex-wrap gap-1.5 mt-3 pt-3 border-t border-border list-none m-0 p-0 pointer-events-none"
>
<TagClickable
<TagButton
v-for="keyword in result.package.keywords.slice(0, 5)"
:key="keyword"
type="button"
class="pointer-events-auto"
:status="props.filters?.keywords.includes(keyword) ? 'active' : 'default'"
:key="keyword"
:pressed="props.filters?.keywords.includes(keyword)"
:title="`Filter by ${keyword}`"
@click.stop="emit('clickKeyword', keyword)"
>
{{ keyword }}
</TagClickable>
</TagButton>
<span
v-if="result.package.keywords.length > 5"
class="text-fg-subtle text-xs pointer-events-auto"
Expand Down
6 changes: 2 additions & 4 deletions app/components/Package/Keywords.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<script setup lang="ts">
import { NuxtLink } from '#components'

defineProps<{
keywords?: string[]
}>()
Expand All @@ -9,9 +7,9 @@ defineProps<{
<CollapsibleSection v-if="keywords?.length" :title="$t('package.keywords_title')" id="keywords">
<ul class="flex flex-wrap gap-1.5 list-none m-0 p-0">
<li v-for="keyword in keywords.slice(0, 15)" :key="keyword">
<TagClickable :as="NuxtLink" :to="{ name: 'search', query: { q: `keywords:${keyword}` } }">
<TagLink :to="{ name: 'search', query: { q: `keywords:${keyword}` } }">
{{ keyword }}
</TagClickable>
</TagLink>
</li>
</ul>
</CollapsibleSection>
Expand Down
7 changes: 3 additions & 4 deletions app/components/Package/TableRow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,15 @@ const allMaintainersText = computed(() => {
class="flex flex-wrap gap-1"
:aria-label="$t('package.card.keywords')"
>
<TagClickable
<TagButton
v-for="keyword in pkg.keywords.slice(0, 3)"
:key="keyword"
type="button"
:status="props.filters?.keywords.includes(keyword) ? 'active' : 'default'"
:pressed="props.filters?.keywords.includes(keyword)"
:title="`Filter by ${keyword}`"
@click.stop="emit('clickKeyword', keyword)"
>
{{ keyword }}
</TagClickable>
</TagButton>
<span
v-if="pkg.keywords.length > 3"
class="text-fg-subtle text-xs"
Expand Down
31 changes: 31 additions & 0 deletions app/components/Tag/Button.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script setup lang="ts">
const props = defineProps<{
disabled?: boolean
/**
* type should never be used, because this will always be a button.
*
* If you want a link use `TagLink` instead.
* */
type?: never
pressed?: boolean
}>()
</script>

<template>
<button
class="inline-flex items-center px-2 py-0.5 text-xs font-mono border rounded transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
:class="[
pressed
? 'bg-fg text-bg border-fg hover:(text-text-bg/50)'
: 'bg-bg-muted text-fg-muted border-border hover:(text-fg border-border-hover)',
{
'opacity-50 cursor-not-allowed': disabled,
},
]"
type="button"
:disabled="disabled ? true : undefined"
:aria-pressed="pressed"
>
<slot />
</button>
</template>
25 changes: 0 additions & 25 deletions app/components/Tag/Clickable.vue

This file was deleted.

36 changes: 36 additions & 0 deletions app/components/Tag/Link.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<script setup lang="ts">
import type { NuxtLinkProps } from '#app'

const { current, ...props } = defineProps<
{
/** Disabled links will be displayed as plain text */
disabled?: boolean
/**
* `type` should never be used, because this will always be a link.
*
* If you want a button use `TagButton` instead.
* */
type?: never
current?: boolean
} &
/** This makes sure the link always has either `to` or `href` */
(Required<Pick<NuxtLinkProps, 'to'>> | Required<Pick<NuxtLinkProps, 'href'>>) &
NuxtLinkProps
>()
</script>

<template>
<span v-if="disabled" class="opacity-50"><slot /></span>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably have a number of the other styles so it looks visually similar or you'd experience layout shift when disabling a button

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was a quick one, because this isn't used anywhere yet. Just wanted to sketch it out.

<NuxtLink
v-else
class="inline-flex items-center px-2 py-0.5 text-xs font-mono border rounded transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-fg focus-visible:ring-offset-1"
:class="{
'bg-bg-muted text-fg-muted border-border hover:(text-fg border-border-hover)': !current,
'bg-fg text-bg border-fg hover:(text-text-bg/50)': current,
'opacity-50 cursor-not-allowed': disabled,
}"
v-bind="props"
>
<slot />
</NuxtLink>
</template>
60 changes: 60 additions & 0 deletions app/components/Tag/RadioButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<script setup lang="ts">
const model = defineModel()

const props = defineProps<{
disabled?: boolean
/**
* type should never be used, because this will always be a button.
*
* If you want a link use `TagLink` instead.
* */
type?: never

/** Shouldn't try to set `checked` explicitly, is handled internally */
checked?: never
value: string
}>()

const uid = useId()
const internalId = `${model.value}-${uid}`
const checked = computed(() => model.value === props.value)
/** Todo: This shouldn't be necessary, but using v-model on `input type=radio` doesn't work as expected in Vue */
const onChange = () => {
model.value = props.value
}
</script>

<template>
<div>
<input
type="radio"
:id="internalId"
:value="props.value"
:checked="checked"
:disabled="props.disabled ? true : undefined"
@change="onChange"
class="peer"
/>
<label
class="bg-bg-muted text-fg-muted border-border hover:(text-fg border-border-hover) inline-flex items-center px-2 py-0.5 text-xs font-mono border rounded transition-colors duration-200 peer-focus:ring-2 peer-focus:ring-fg border-none peer-checked:(bg-fg text-bg border-fg hover:(text-text-bg/50)) peer-disabled:(opacity-50 pointer-events-none)"
:htmlFor="internalId"
>
<slot />
</label>
</div>
</template>

<style scoped>
input[type='radio'] {
position: absolute;
opacity: 0;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
</style>
Loading
Loading