diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 282108089b..a01c7cbd1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -440,24 +440,26 @@ The command palette is a first-class navigation surface. When you add a new user #### Available route names -| Route name | URL pattern | Parameters | -| ------------------- | ------------------------------------------------- | -------------------------------- | -| `index` | `/` | — | -| `about` | `/about` | — | -| `compare` | `/compare` | — | -| `privacy` | `/privacy` | — | -| `search` | `/search` | — | -| `settings` | `/settings` | — | -| `package` | `/package/:org?/:name` | `org?`, `name` | -| `package-version` | `/package/:org?/:name/v/:version` | `org?`, `name`, `version` | -| `code` | `/package-code/:path+` | `path` (array) | -| `docs` | `/package-docs/:path+` | `path` (array) | -| `changelog` | `/package-changelog/:org?/:name` | `org?`, `name` | -| `changelog-version` | `/package-changelog/:org?/:name/v/:version` | `org?`, `name`, `version` | -| `timeline` | `/package-timeline/:org?/:packageName/v/:version` | `org?`, `packageName`, `version` | -| `org` | `/org/:org` | `org` | -| `~username` | `/~:username` | `username` | -| `~username-orgs` | `/~:username/orgs` | `username` | +| Route name | URL pattern | Parameters | +| ----------------------------- | ------------------------------------------------- | ------------------------------------ | +| `index` | `/` | — | +| `about` | `/about` | — | +| `compare` | `/compare` | — | +| `privacy` | `/privacy` | — | +| `search` | `/search` | — | +| `settings` | `/settings` | — | +| `package` | `/package/:org?/:name` | `org?`, `name` | +| `package-version` | `/package/:org?/:name/v/:version` | `org?`, `name`, `version` | +| `code` | `/package-code/:path+` | `path` (array) | +| `docs` | `/package-docs/:path+` | `path` (array) | +| `changelog` | `/package-changelog/:org?/:name` | `org?`, `name` | +| `changelog-version` | `/package-changelog/:org?/:name/v/:version` | `org?`, `name`, `version` | +| `timeline` | `/package-timeline/:org?/:packageName/v/:version` | `org?`, `packageName`, `version` | +| `org` | `/org/:org` | `org` | +| `~username` | `/~:username` | `username` | +| `~username-orgs` | `/~:username/orgs` | `username` | +| `collections-new` | `/collections/new` | — | +| `collections-identifier-rkey` | `/collections/:identifier/:rkey` | `identifier` (handle or DID), `rkey` | ### Cursor and navigation diff --git a/app/components/Collection/Card.stories.ts b/app/components/Collection/Card.stories.ts new file mode 100644 index 0000000000..1ba304a672 --- /dev/null +++ b/app/components/Collection/Card.stories.ts @@ -0,0 +1,19 @@ +import type { Meta, StoryObj } from '@storybook-vue/nuxt' +import Card from './Card.vue' +import { mockCollectionSummary } from '../../storybook/mocks/handlers/collections' + +const meta = { + component: Card, + tags: ['autodocs'], + args: { collection: mockCollectionSummary }, + decorators: [() => ({ template: '
' })], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const WithoutDescription: Story = { + args: { collection: { ...mockCollectionSummary, description: undefined } }, +} diff --git a/app/components/Collection/Card.vue b/app/components/Collection/Card.vue new file mode 100644 index 0000000000..c1734969dc --- /dev/null +++ b/app/components/Collection/Card.vue @@ -0,0 +1,40 @@ + + + diff --git a/app/components/Collection/Editor.stories.ts b/app/components/Collection/Editor.stories.ts new file mode 100644 index 0000000000..eb9d107853 --- /dev/null +++ b/app/components/Collection/Editor.stories.ts @@ -0,0 +1,42 @@ +import type { Meta, StoryObj } from '@storybook-vue/nuxt' +import { expect, userEvent } from 'storybook/test' +import Editor from './Editor.vue' +import { mockCollection } from '../../storybook/mocks/handlers/collections' + +const meta = { + component: Editor, + tags: ['autodocs'], + decorators: [() => ({ template: '
' })], +} satisfies Meta + +export default meta +type Story = StoryObj + +/** Creating a new collection. */ +export const Empty: Story = {} + +/** Editing an existing collection. */ +export const Editing: Story = { + args: { initial: mockCollection }, +} + +/** The collection changed elsewhere while it was open. */ +export const Conflict: Story = { + args: { + ...Editing.args, + errorMessage: + 'This collection was changed somewhere else since you opened it. Reload to get the latest version.', + }, +} + +/** Adding a package by name and reordering it to the top. */ +export const AddAndReorder: Story = { + args: Editing.args, + play: async ({ canvas }) => { + await userEvent.type(canvas.getByRole('combobox', { name: /add a package/i }), 'zod{enter}') + // Only added items have this; text matching can hit a typeahead suggestion that then closes + const moveUp = await canvas.findByRole('button', { name: /move zod up/i }) + await userEvent.click(moveUp) + await expect(canvas.getByRole('button', { name: /move zod down/i })).toBeVisible() + }, +} diff --git a/app/components/Collection/Editor.vue b/app/components/Collection/Editor.vue new file mode 100644 index 0000000000..33c7764b74 --- /dev/null +++ b/app/components/Collection/Editor.vue @@ -0,0 +1,307 @@ + + + diff --git a/app/components/Collection/Grid.vue b/app/components/Collection/Grid.vue new file mode 100644 index 0000000000..a6e0441aa2 --- /dev/null +++ b/app/components/Collection/Grid.vue @@ -0,0 +1,71 @@ + + + diff --git a/app/components/Collection/ItemList.stories.ts b/app/components/Collection/ItemList.stories.ts new file mode 100644 index 0000000000..1569dc6e1b --- /dev/null +++ b/app/components/Collection/ItemList.stories.ts @@ -0,0 +1,44 @@ +import type { Meta, StoryObj } from '@storybook-vue/nuxt' +import ItemList from './ItemList.vue' +import { mockCollection, mockPackageCards } from '../../storybook/mocks/handlers/collections' + +const meta = { + component: ItemList, + tags: ['autodocs'], + args: { items: mockCollection.items }, + decorators: [() => ({ template: '
' })], +} satisfies Meta + +export default meta +type Story = StoryObj + +/** Packages with notes, a package missing from the registry, and a nested collection. */ +export const Default: Story = {} + +/** Packages shown as cards once their metadata has loaded. */ +export const WithPackageCards: Story = { + args: { packages: mockPackageCards }, +} + +/** An included collection that can't be shown. */ +export const UnavailableCollection: Story = { + args: { + items: [ + { + kind: 'collection', + subjectRef: 'at://did:plc:x/dev.npmx.collection/3m5k5baifas2z', + collection: null, + unavailableReason: 'unresolvable', + }, + ], + }, +} + +/** Items written by other clients that we don't understand. */ +export const UnknownItems: Story = { + args: { + items: [ + { kind: 'unknown', subjectRef: 'https://www.npmjs.com/package/zod', note: 'Wrong URL' }, + ], + }, +} diff --git a/app/components/Collection/ItemList.vue b/app/components/Collection/ItemList.vue new file mode 100644 index 0000000000..eb59e397cf --- /dev/null +++ b/app/components/Collection/ItemList.vue @@ -0,0 +1,101 @@ + + + diff --git a/app/components/Collection/ItemNote.vue b/app/components/Collection/ItemNote.vue new file mode 100644 index 0000000000..28ec5aeded --- /dev/null +++ b/app/components/Collection/ItemNote.vue @@ -0,0 +1,8 @@ + diff --git a/app/components/Collection/YourData.vue b/app/components/Collection/YourData.vue new file mode 100644 index 0000000000..47bd584ee5 --- /dev/null +++ b/app/components/Collection/YourData.vue @@ -0,0 +1,27 @@ + + + diff --git a/app/components/Header/AuthModal.client.vue b/app/components/Header/AuthModal.client.vue index dc395b1574..5ec7aad833 100644 --- a/app/components/Header/AuthModal.client.vue +++ b/app/components/Header/AuthModal.client.vue @@ -10,11 +10,8 @@ const errorMessage = shallowRef('') const route = useRoute() const { user, logout } = useAtproto() -// https://atproto.com supports 4 locales as of 2026-02-07 const { locale } = useI18n() -const currentLang = locale.value.split('-')[0] ?? 'en' -const localeSubPath = ['ko', 'pt', 'ja'].includes(currentLang) ? currentLang : '' -const atprotoLink = `https://atproto.com/${localeSubPath}` +const atprotoLink = useAtprotoSiteUrl() async function handleBlueskySignIn() { await authRedirect('https://bsky.social', { redirectTo: route.fullPath, locale: locale.value }) diff --git a/app/components/Package/Card.vue b/app/components/Package/Card.vue index 76754a6dfa..c9d5ea4b51 100644 --- a/app/components/Package/Card.vue +++ b/app/components/Package/Card.vue @@ -46,24 +46,27 @@ const numberFormatter = useNumberFormatter()