vyriy 0.4.11 → 0.5.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.
@@ -1,16 +1,333 @@
1
+ import packageJson from '../../../package.json' with { type: 'json' };
1
2
  import { base } from './base.js';
3
+ const projectFiles = {
4
+ '.browserslistrc': '[development]\nextends @vyriy/browserslist-config\n\n[ssr]\nextends @vyriy/browserslist-config\n\n[production]\nextends @vyriy/browserslist-config\n\n[modern]\nextends @vyriy/browserslist-config\n',
5
+ '.storybook/preview.tsx': "import '../packages/components/styles.scss';\n\nexport { default } from '@vyriy/storybook-config/preview';\n",
6
+ 'packages/api/demo.test.ts': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport { demo } from './demo.js';\n\ndescribe('demo', () => {\n afterEach(() => {\n delete process.env.MODE;\n delete process.env.TAG;\n delete process.env.UI;\n });\n\n it('builds the profile-card demo document', () => {\n process.env.MODE = 'open';\n process.env.TAG = 'vyriy-profile-card';\n process.env.UI = 'http://localhost:3002';\n\n const document = demo();\n\n expect(document).toContain('<!DOCTYPE html>');\n expect(document).toContain('<vyriy-profile-card');\n expect(document).toContain('name=\"Developer\"');\n expect(document).toContain('rendered');\n expect(document).toContain('http://localhost:3002/index.js');\n expect(document).toContain('vyriy-profile-card.select');\n });\n});\n",
7
+ 'packages/api/demo.ts': "import { html } from '@vyriy/html';\n\nimport { getUi } from '@p/env';\n\nimport { prerender } from './prerender.js';\nimport type { Demo } from './types.js';\n\n/** Builds the standalone HTML demo page for the profile-card microfrontend. */\nexport const demo: Demo = () =>\n html({\n htmlAttributes: 'lang=\"en\"',\n title: '<title>Demo</title>',\n meta: '<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n body: prerender({\n name: 'Developer',\n title: 'Senior IT Professional',\n avatarUrl: 'http://localhost:3001/avatar.svg',\n }),\n script: [\n `<script defer=\"defer\" src=\"${getUi()}/index.js\"></script>`,\n `<script type=\"module\">\n const card = document.querySelector('vyriy-profile-card');\n card.addEventListener('vyriy-profile-card.select', (event) => {\n console.log('UI demo event:', event.detail);\n });\n </script>`,\n ].join(''),\n });\n",
8
+ 'packages/api/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Packages/API\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
9
+ 'packages/api/index.test.ts': "import { describe, expect, it } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('api public API', () => {\n it('exports API document builders', () => {\n expect(publicApi.demo).toBeDefined();\n expect(publicApi.manifest).toBeDefined();\n expect(publicApi.prerender).toBeDefined();\n expect(publicApi.semantic).toBeDefined();\n });\n});\n",
10
+ 'packages/api/index.ts': "export * from './demo.js';\nexport * from './manifest.js';\nexport * from './prerender.js';\nexport * from './semantic.js';\n\nexport type * from './types.js';\n",
11
+ 'packages/api/manifest.test.ts': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport { manifest } from './manifest.js';\n\ndescribe('manifest', () => {\n afterEach(() => {\n delete process.env.API;\n delete process.env.CDN;\n delete process.env.UI;\n });\n\n it('builds the OpenMFE manifest YAML', () => {\n process.env.API = 'http://localhost:3000';\n process.env.CDN = 'http://localhost:3001';\n process.env.UI = 'http://localhost:3002';\n\n const document = manifest();\n\n expect(document).toContain(\"version: '1.1'\");\n expect(document).toContain(\"source: 'http://localhost:3002/index.js'\");\n expect(document).toContain(\"prerender: 'http://localhost:3000/prerender'\");\n expect(document).toContain(\"icon: 'http://localhost:3001/icon.svg'\");\n expect(document).toContain(\"name: 'vyriy-profile-card.select'\");\n expect(document).toContain(\"name: 'openmfe.analytics'\");\n });\n});\n",
12
+ 'packages/api/manifest.ts': "import { getUi, getApi, getCdn } from '@p/env';\nimport { PROFILE_CARD_ANALYTICS_EVENT_NAME, PROFILE_CARD_SELECT_EVENT_NAME } from '@p/event';\n\nimport type { Manifest } from './types.js';\n\n/** Builds the OpenMFE manifest YAML for the profile-card microfrontend. */\nexport const manifest: Manifest = () => `version: '1.1'\nname: 'Vyriy Profile Card'\nsource: '${getUi()}/index.js'\ntag: 'vyriy-profile-card'\nurl:\n frontend: '${getUi()}/index.js'\n prerender: '${getApi()}/prerender'\n semantic: '${getApi()}/semantic'\npublisher:\n name: 'Vyriy'\n email: 'vyriy@vyriy.dev'\nicon: '${getCdn()}/icon.svg'\ndescription: 'A small OpenMFE-oriented profile card starter for Vyriy CLI.'\ndocumentation: 'https://vyriy.dev/'\nattributes:\n - name: 'name'\n description: 'Profile display name.'\n required: true\n schema:\n type: 'string'\n minLength: 1\n - name: 'role'\n description: 'Short profile role or title.'\n required: false\n schema:\n type: 'string'\n default: 'Developer'\n - name: 'avatar-url'\n description: 'Optional avatar image URL.'\n required: false\n schema:\n type: 'string'\n format: 'uri-reference'\n default: '${getCdn()}/avatar.svg'\nevents:\n - name: '${PROFILE_CARD_SELECT_EVENT_NAME}'\n description: 'Emitted when the user selects the profile card.'\n schema:\n type: 'object'\n required:\n - name\n properties:\n name:\n type: 'string'\n description: 'Selected profile display name.'\n role:\n type: 'string'\n description: 'Selected profile role.'\n avatarUrl:\n type:\n - 'string'\n - 'null'\n description: 'Selected profile avatar URL.'\n - name: '${PROFILE_CARD_ANALYTICS_EVENT_NAME}'\n description: 'Standard OpenMFE analytics event emitted for profile card interactions.'\n schema:\n type: 'object'\n required:\n - name\n - origin\n - id\n - variant\n - action\n - category\n - data\n properties:\n name:\n type: 'string'\n description: 'Analytics event name.'\n origin:\n type: 'string'\n description: 'Microfrontend tag name.'\n id:\n type:\n - 'string'\n - 'null'\n description: 'Component instance id or null.'\n variant:\n type:\n - 'string'\n - 'null'\n description: 'Experiment variant or null.'\n action:\n type: 'string'\n description: 'User action.'\n category:\n type:\n - 'string'\n - 'null'\n description: 'Analytics category or null.'\n data:\n type: 'object'\n description: 'Custom analytics payload.'\nfunctions:\n - name: 'refresh'\n description: 'Refreshes the current profile card rendering.'\n async: true\n parameters: []\n return:\n description: 'Refresh result.'\n schema:\n type: 'object'\n properties:\n ok:\n type: 'boolean'\n description: 'Whether refresh completed successfully.'\nsemantic:\n type: 'object'\n required:\n - '@context'\n - '@type'\n - 'name'\n properties:\n '@context':\n type: 'string'\n description: 'JSON-LD context URL.'\n '@type':\n type: 'string'\n description: 'Schema.org entity type.'\n name:\n type: 'string'\n description: 'Profile display name.'\n jobTitle:\n type: 'string'\n description: 'Profile role or title.'\n image:\n type: 'string'\n description: 'Profile avatar URL.'\nscreenshots:\n - url: '${getCdn()}/screenshots/default.svg'\n description: 'Default profile card rendering.'\n - url: '${getCdn()}/screenshots/compact.svg'\n description: 'Compact profile card rendering.'\nexamples:\n - description: 'Default profile card.'\n attributes:\n name: 'Ada Lovelace'\n role: 'Mathematician'\n avatar-url: '${getCdn()}/avatar.svg'\nrepository: 'https://github.com/evheniy/vyriy'`;\n",
13
+ 'packages/api/package.json': '{\n "name": "@p/api",\n "type": "module",\n "private": true\n}\n',
14
+ 'packages/api/prerender.test.tsx': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport { prerender } from './prerender.js';\n\ndescribe('prerender', () => {\n afterEach(() => {\n delete process.env.MODE;\n delete process.env.TAG;\n delete process.env.UI;\n });\n\n it('renders profile card props as custom element attributes', () => {\n process.env.MODE = 'open';\n process.env.TAG = 'vyriy-profile-card';\n process.env.UI = 'http://localhost:3002';\n\n expect(\n prerender({\n avatarUrl: 'https://example.com/avatar.png',\n description: 'A \"quoted\" <description>',\n name: 'Name',\n tags: ['one', 'two'],\n title: 'Title',\n }),\n ).toContain(\n '<vyriy-profile-card name=\"Name\" title=\"Title\" description=\"A &quot;quoted&quot; &lt;description&gt;\" avatarUrl=\"https://example.com/avatar.png\" tags=\"[&quot;one&quot;,&quot;two&quot;]\" rendered>',\n );\n expect(\n prerender({\n name: 'Name',\n }),\n ).toContain('<div data-vyriy-root>');\n });\n\n it('removes React generated resource hints from the hydration root', () => {\n process.env.MODE = 'open';\n process.env.TAG = 'vyriy-profile-card';\n process.env.UI = 'http://localhost:3002';\n\n expect(\n prerender({\n avatarUrl: 'https://example.com/avatar.png',\n name: 'Name',\n }),\n ).not.toContain('rel=\"preload\"');\n });\n});\n",
15
+ 'packages/api/prerender.tsx': "import { html } from '@vyriy/render';\n\nimport { ProfileCard } from '@p/components/profile-card';\n\nimport { getTag, getMode, getUi } from '@p/env';\n\nimport type { Prerender } from './types.js';\n\ntype AttributeValue = string | string[] | undefined;\n\nconst escapeAttribute = (value: string): string => {\n return value.replaceAll('&', '&amp;').replaceAll('\"', '&quot;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');\n};\n\nconst getAttributeValue = (value: AttributeValue): string | undefined => {\n if (value === undefined) {\n return undefined;\n }\n\n return Array.isArray(value) ? JSON.stringify(value) : value;\n};\n\nconst getAttribute = (name: string, value: AttributeValue): string => {\n const attributeValue = getAttributeValue(value);\n\n if (attributeValue === undefined) {\n return '';\n }\n\n return ` ${name}=\"${escapeAttribute(attributeValue)}\"`;\n};\n\nconst getProfileCardAttributes = (props: Parameters<Prerender>[0]): string => {\n return [\n getAttribute('name', props.name),\n getAttribute('title', props.title),\n getAttribute('description', props.description),\n getAttribute('avatarUrl', props.avatarUrl),\n getAttribute('tags', props.tags),\n ].join('');\n};\n\nconst removeReactResourceHints = (value: string): string => {\n return value.replaceAll(/<link\\b(?=[^>]*\\brel=\"preload\")[^>]*>/g, '');\n};\n\n/** Renders a profile card custom element with declarative shadow DOM for hydration. */\nexport const prerender: Prerender = (props) => {\n const tag = getTag();\n const mode = getMode();\n const body = removeReactResourceHints(html(<ProfileCard {...props} />));\n\n return [\n `<${tag}${getProfileCardAttributes(props)} rendered>`,\n `<template shadowrootmode=\"${mode}\">`,\n `<link rel=\"stylesheet\" href=\"${getUi()}/main.css\" />`,\n `<div data-vyriy-root>${body}</div>`,\n `</template>`,\n `</${tag}>`,\n ].join('');\n};\n",
16
+ 'packages/api/README.md': "# @p/api\n\nServer-side document builders for the profile-card microfrontend.\n\n## Exports\n\n- `demo()` returns a standalone HTML demo document.\n- `prerender(props)` returns SSR custom element markup with declarative shadow DOM.\n- `semantic(props)` returns Schema.org Person JSON-LD.\n- `manifest()` returns the OpenMFE manifest YAML.\n\n## Example\n\n```ts\nimport { prerender } from '@p/api';\n\nconst html = prerender({\n name: 'Ada Lovelace',\n title: 'Mathematician',\n});\n```\n",
17
+ 'packages/api/semantic.test.ts': "import { describe, expect, it } from '@jest/globals';\n\nimport { semantic } from './semantic.js';\n\ndescribe('semantic', () => {\n it('serializes profile-card props as Schema.org JSON-LD', () => {\n expect(\n JSON.parse(\n semantic({\n avatarUrl: 'https://example.com/avatar.png',\n name: 'Ada Lovelace',\n title: 'Mathematician',\n }),\n ),\n ).toEqual({\n '@context': 'https://schema.org',\n '@type': 'Person',\n image: 'https://example.com/avatar.png',\n jobTitle: 'Mathematician',\n name: 'Ada Lovelace',\n });\n });\n});\n",
18
+ 'packages/api/semantic.ts': "import type { Semantic } from './types.js';\n\n/** Serializes profile-card props as Schema.org Person JSON-LD. */\nexport const semantic: Semantic = (params) =>\n JSON.stringify({\n '@context': 'https://schema.org',\n '@type': 'Person',\n name: params.name,\n jobTitle: params.title,\n image: params.avatarUrl,\n });\n",
19
+ 'packages/api/types.ts': "import type { ProfileCard } from '@p/components/profile-card/types.js';\n\n/** Builds the standalone HTML demo document. */\nexport type Demo = () => string;\n\n/** Builds server-rendered custom element markup from profile-card props. */\nexport type Prerender = (props: ProfileCard) => string;\n\n/** Builds semantic JSON-LD from profile-card props. */\nexport type Semantic = (props: ProfileCard) => string;\n\n/** Builds the OpenMFE manifest YAML document. */\nexport type Manifest = () => string;\n",
20
+ 'packages/components/avatar/avatar.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { Avatar } from './avatar.js';\nimport avatar from '../../../workspaces/static/public/avatar.svg';\n\nconst meta = {\n title: 'Components/Avatar',\n component: Avatar,\n parameters: { docs: { page: null } },\n args: { name: 'Ada Lovelace', size: 'md' },\n} satisfies Meta<typeof Avatar>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Image: Story = {\n args: {\n src: avatar,\n alt: 'Profile portrait',\n },\n};\n\nexport const Large: Story = { args: { size: 'lg' } };\n",
21
+ 'packages/components/avatar/avatar.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { Avatar } from './avatar.js';\n\ndescribe('Avatar', () => {\n it('renders an image when src exists', () => {\n render(<Avatar src=\"/avatar.png\" name=\"Ada Lovelace\" />);\n\n expect(screen.getByRole('img', { name: 'Ada Lovelace' })).toBeDefined();\n });\n\n it('uses a generic image alt when name and alt are not provided', () => {\n render(<Avatar src=\"/avatar.png\" />);\n\n expect(screen.getByRole('img', { name: 'Profile avatar' })).toBeDefined();\n });\n\n it('renders initials fallback without exposing decorative text', () => {\n render(<Avatar name=\"Ada Lovelace\" data-testid=\"avatar-root\" />);\n\n expect(screen.getByText('AL')).toBeDefined();\n expect(screen.getByText('AL').getAttribute('aria-hidden')).toBe('true');\n expect(screen.getByTestId('avatar-root')).toBeDefined();\n });\n\n it('renders a stable fallback when name is not provided', () => {\n render(<Avatar />);\n\n expect(screen.getByText('?')).toBeDefined();\n });\n\n it('renders a stable fallback when name has no initials', () => {\n render(<Avatar name=\" \" />);\n\n expect(screen.getByText('?')).toBeDefined();\n });\n});\n",
22
+ 'packages/components/avatar/avatar.tsx': "import { cn } from '@vyriy/cn';\n\nimport type { AvatarType } from './types.js';\n\nconst getInitials = (name?: string) => {\n if (!name) {\n return '?';\n }\n\n const initials = name\n .trim()\n .split(/\\s+/)\n .filter(Boolean)\n .slice(0, 2)\n .map((part) => part[0]?.toUpperCase())\n .join('');\n\n return initials || '?';\n};\n\n/** Renders either a profile image or deterministic initials fallback. */\nexport const Avatar: AvatarType = ({ src, alt, name, size = 'md', className, ...props }) => {\n return (\n <div className={cn('avatar', `avatar--${size}`, className)} {...props}>\n {src ? (\n <img className=\"avatar__image\" alt={alt ?? name ?? 'Profile avatar'} src={src} />\n ) : (\n <span className=\"avatar__initials\" aria-hidden=\"true\">\n {getInitials(name)}\n </span>\n )}\n </div>\n );\n};\n",
23
+ 'packages/components/avatar/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as AvatarStories from './avatar.stories';\n\n<Meta of={AvatarStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={AvatarStories.Default} />\n<Canvas of={AvatarStories.Image} />\n<Canvas of={AvatarStories.Large} />\n",
24
+ 'packages/components/avatar/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('avatar public API', () => {\n it('exports Avatar', () => {\n expect(publicApi.Avatar).toBeDefined();\n });\n});\n",
25
+ 'packages/components/avatar/index.ts': "export * from './avatar.js';\nexport type * from './types.js';\n",
26
+ 'packages/components/avatar/README.md': "# Avatar\n\n`Avatar` renders a profile image or deterministic initials fallback.\n\n## Usage\n\n```tsx\nimport { Avatar } from './avatar.js';\n\nexport const Example = () => <Avatar name=\"Ada Lovelace\" />;\n```\n\n## Props\n\n```ts\nexport type AvatarProps = {\n src?: string;\n alt?: string;\n name?: string;\n size?: 'sm' | 'md' | 'lg';\n} & ComponentProps<'div'>;\n```\n\n## Accessibility\n\nImage avatars receive useful alt text. Initials are decorative and hidden from assistive technology.\n\n## Notes\n\n- SSR/SSG-safe.\n- No browser-only APIs.\n- Shared SCSS styles.\n",
27
+ 'packages/components/avatar/styles.scss': '.avatar {\n display: inline-grid;\n flex: 0 0 auto;\n place-items: center;\n overflow: hidden;\n border: 1px solid var(--profile-card-border-color, #e5e7eb);\n border-radius: 999px;\n background: var(--profile-card-muted-background, #f9fafb);\n color: var(--profile-card-color, #111827);\n font-weight: 700;\n letter-spacing: 0;\n line-height: 1;\n}\n\n.avatar--sm {\n width: 2rem;\n height: 2rem;\n font-size: 0.75rem;\n}\n\n.avatar--md {\n width: 3rem;\n height: 3rem;\n font-size: 1rem;\n}\n\n.avatar--lg {\n width: 4rem;\n height: 4rem;\n font-size: 1.25rem;\n}\n\n.avatar__image {\n display: block;\n width: 100%;\n height: 100%;\n object-fit: cover;\n}\n\n.avatar__initials {\n display: inline-block;\n}\n',
28
+ 'packages/components/avatar/types.ts': "import type { ComponentProps, FC } from 'react';\n\n/** Props for the Avatar component. */\nexport type AvatarProps = {\n src?: string;\n alt?: string;\n name?: string;\n size?: 'sm' | 'md' | 'lg';\n} & ComponentProps<'div'>;\n\n/** Avatar component type. */\nexport type AvatarType = FC<AvatarProps>;\n",
29
+ 'packages/components/badge/badge.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { Badge } from './badge.js';\n\nconst meta = {\n title: 'Components/Badge',\n component: Badge,\n parameters: { docs: { page: null } },\n args: { children: 'React', tone: 'neutral' },\n} satisfies Meta<typeof Badge>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Blue: Story = { args: { tone: 'blue' } };\n\nexport const Green: Story = { args: { tone: 'green', children: 'Available' } };\n\nexport const Amber: Story = { args: { tone: 'amber', children: 'Maintainer' } };\n\nexport const Red: Story = { args: { tone: 'red', children: 'Busy' } };\n",
30
+ 'packages/components/badge/badge.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { Badge } from './badge.js';\n\ndescribe('Badge', () => {\n it('renders label content', () => {\n render(<Badge tone=\"green\">Available</Badge>);\n\n expect(screen.getByText('Available')).toBeDefined();\n });\n\n it('passes span props to the root element', () => {\n render(<Badge data-testid=\"badge-root\">React</Badge>);\n\n expect(screen.getByTestId('badge-root')).toBeDefined();\n });\n});\n",
31
+ 'packages/components/badge/badge.tsx': "import { cn } from '@vyriy/cn';\n\nimport type { BadgeType } from './types.js';\n\n/** Renders a compact tone-aware label. */\nexport const Badge: BadgeType = ({ children, tone = 'neutral', className, ...props }) => {\n return (\n <span className={cn('badge', `badge--${tone}`, className)} {...props}>\n {children}\n </span>\n );\n};\n",
32
+ 'packages/components/badge/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as BadgeStories from './badge.stories';\n\n<Meta of={BadgeStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={BadgeStories.Default} />\n<Canvas of={BadgeStories.Blue} />\n<Canvas of={BadgeStories.Green} />\n<Canvas of={BadgeStories.Amber} />\n<Canvas of={BadgeStories.Red} />\n",
33
+ 'packages/components/badge/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('badge public API', () => {\n it('exports Badge', () => {\n expect(publicApi.Badge).toBeDefined();\n });\n});\n",
34
+ 'packages/components/badge/index.ts': "export * from './badge.js';\nexport type * from './types.js';\n",
35
+ 'packages/components/badge/README.md': "# Badge\n\n`Badge` displays compact labels for tags, roles, and statuses.\n\n## Usage\n\n```tsx\nimport { Badge } from './badge.js';\n\nexport const Example = () => <Badge tone=\"green\">Available</Badge>;\n```\n\n## Props\n\n```ts\nexport type BadgeProps = {\n children: ReactNode;\n tone?: 'neutral' | 'blue' | 'green' | 'amber' | 'red';\n} & ComponentProps<'span'>;\n```\n\n## Notes\n\n- SSR/SSG-safe.\n- Root element accepts regular `span` props.\n- Shared SCSS styles with tone variants.\n",
36
+ 'packages/components/badge/styles.scss': '.badge {\n display: inline-flex;\n align-items: center;\n max-width: 100%;\n border: 1px solid transparent;\n border-radius: 999px;\n font-size: 0.75rem;\n font-weight: 650;\n letter-spacing: 0;\n line-height: 1.2;\n padding: 0.25rem 0.55rem;\n white-space: nowrap;\n}\n\n.badge--neutral {\n border-color: var(--profile-card-border-color, #e5e7eb);\n background: var(--profile-card-muted-background, #f9fafb);\n color: var(--profile-card-color, #111827);\n}\n\n.badge--blue {\n border-color: #bfdbfe;\n background: #eff6ff;\n color: #1d4ed8;\n}\n\n.badge--green {\n border-color: #bbf7d0;\n background: #f0fdf4;\n color: #15803d;\n}\n\n.badge--amber {\n border-color: #fde68a;\n background: #fffbeb;\n color: #a16207;\n}\n\n.badge--red {\n border-color: #fecaca;\n background: #fef2f2;\n color: #b91c1c;\n}\n',
37
+ 'packages/components/badge/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Props for the Badge component. */\nexport type BadgeProps = {\n children: ReactNode;\n tone?: 'neutral' | 'blue' | 'green' | 'amber' | 'red';\n} & ComponentProps<'span'>;\n\n/** Badge component type. */\nexport type BadgeType = FC<BadgeProps>;\n",
38
+ 'packages/components/button-link/button-link.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ButtonLink } from './button-link.js';\n\nconst meta = {\n title: 'Components/ButtonLink',\n component: ButtonLink,\n parameters: { docs: { page: null } },\n args: { href: '#', children: 'Open profile', variant: 'primary' },\n} satisfies Meta<typeof ButtonLink>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Secondary: Story = { args: { variant: 'secondary' } };\n\nexport const Ghost: Story = { args: { variant: 'ghost' } };\n",
39
+ 'packages/components/button-link/button-link.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ButtonLink } from './button-link.js';\n\ndescribe('ButtonLink', () => {\n it('renders a native link', () => {\n render(<ButtonLink href=\"/profile\">Open profile</ButtonLink>);\n\n expect(screen.getByRole('link', { name: 'Open profile' }).getAttribute('href')).toBe('/profile');\n });\n\n it('adds external link attributes', () => {\n render(\n <ButtonLink external href=\"https://example.com\">\n Website\n </ButtonLink>,\n );\n\n const link = screen.getByRole('link', { name: 'Website' });\n\n expect(link.getAttribute('target')).toBe('_blank');\n expect(link.getAttribute('rel')).toBe('noreferrer');\n });\n});\n",
40
+ 'packages/components/button-link/button-link.tsx': "import { cn } from '@vyriy/cn';\n\nimport type { ButtonLinkType } from './types.js';\n\n/** Renders an anchor styled as a button. */\nexport const ButtonLink: ButtonLinkType = ({\n href,\n children,\n variant = 'primary',\n external = false,\n className,\n ...props\n}) => {\n return (\n <a\n className={cn('button-link', `button-link--${variant}`, className)}\n href={href}\n target={external ? '_blank' : props.target}\n rel={external ? 'noreferrer' : props.rel}\n {...props}\n >\n {children}\n </a>\n );\n};\n",
41
+ 'packages/components/button-link/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ButtonLinkStories from './button-link.stories';\n\n<Meta of={ButtonLinkStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ButtonLinkStories.Default} />\n<Canvas of={ButtonLinkStories.Secondary} />\n<Canvas of={ButtonLinkStories.Ghost} />\n",
42
+ 'packages/components/button-link/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('button-link public API', () => {\n it('exports ButtonLink', () => {\n expect(publicApi.ButtonLink).toBeDefined();\n });\n});\n",
43
+ 'packages/components/button-link/index.ts': "export * from './button-link.js';\nexport type * from './types.js';\n",
44
+ 'packages/components/button-link/README.md': "# ButtonLink\n\n`ButtonLink` keeps native anchor semantics while using button-like styling.\n\n## Usage\n\n```tsx\nimport { ButtonLink } from './button-link.js';\n\nexport const Example = () => <ButtonLink href=\"/profile\">Open profile</ButtonLink>;\n```\n\n## Props\n\n```ts\nexport type ButtonLinkProps = {\n href: string;\n children: ReactNode;\n variant?: 'primary' | 'secondary' | 'ghost';\n external?: boolean;\n} & ComponentProps<'a'>;\n```\n\n## Accessibility\n\nThe component renders a native `<a>`. External links receive `target=\"_blank\"` and `rel=\"noreferrer\"`.\n\n## Notes\n\n- SSR/SSG-safe.\n- Visible focus style.\n- Shared SCSS styles.\n",
45
+ 'packages/components/button-link/styles.scss': '.button-link {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-height: 2.25rem;\n border: 1px solid transparent;\n border-radius: 0.6rem;\n font-size: 0.875rem;\n font-weight: 650;\n letter-spacing: 0;\n line-height: 1.2;\n padding: 0.55rem 0.85rem;\n text-decoration: none;\n transition:\n background-color 140ms ease,\n border-color 140ms ease,\n color 140ms ease;\n}\n\n.button-link:focus-visible {\n outline: none;\n box-shadow: var(--profile-card-focus-ring, 0 0 0 3px rgb(59 130 246 / 30%));\n}\n\n.button-link--primary {\n background: var(--profile-card-color, #111827);\n color: var(--profile-card-background, #ffffff);\n}\n\n.button-link--primary:hover {\n background: #374151;\n}\n\n.button-link--secondary {\n border-color: var(--profile-card-border-color, #e5e7eb);\n background: var(--profile-card-background, #ffffff);\n color: var(--profile-card-color, #111827);\n}\n\n.button-link--secondary:hover {\n background: var(--profile-card-muted-background, #f9fafb);\n}\n\n.button-link--ghost {\n background: transparent;\n color: var(--profile-card-color, #111827);\n}\n\n.button-link--ghost:hover {\n background: var(--profile-card-muted-background, #f9fafb);\n}\n',
46
+ 'packages/components/button-link/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Props for the ButtonLink component. */\nexport type ButtonLinkProps = {\n href: string;\n children: ReactNode;\n variant?: 'primary' | 'secondary' | 'ghost';\n external?: boolean;\n} & ComponentProps<'a'>;\n\n/** ButtonLink component type. */\nexport type ButtonLinkType = FC<ButtonLinkProps>;\n",
47
+ 'packages/components/card/card.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { Card } from './card.js';\n\nconst meta = {\n title: 'Components/Card',\n component: Card,\n parameters: { docs: { page: null } },\n args: {\n title: 'Profile',\n subtitle: 'Calm reusable UI primitive',\n children: 'Card content',\n },\n} satisfies Meta<typeof Card>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Muted: Story = { args: { variant: 'muted' } };\n\nexport const Highlighted: Story = { args: { variant: 'highlighted' } };\n",
48
+ 'packages/components/card/card.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { Card } from './card.js';\n\ndescribe('Card', () => {\n it('renders title, subtitle, and children', () => {\n render(\n <Card title=\"Profile\" subtitle=\"Frontend engineer\">\n <span>Content</span>\n </Card>,\n );\n\n expect(screen.getByRole('heading', { name: 'Profile' })).toBeDefined();\n expect(screen.getByText('Frontend engineer')).toBeDefined();\n expect(screen.getByText('Content')).toBeDefined();\n });\n\n it('passes div props to the root element', () => {\n render(<Card title=\"Profile\" data-testid=\"card-root\" />);\n\n expect(screen.getByTestId('card-root')).toBeDefined();\n });\n});\n",
49
+ 'packages/components/card/card.tsx': 'import { cn } from \'@vyriy/cn\';\n\nimport type { CardType } from \'./types.js\';\n\n/** Renders a small content surface with optional heading text. */\nexport const Card: CardType = ({ title, subtitle, children, variant = \'default\', className, ...props }) => {\n return (\n <div className={cn(\'card\', `card--${variant}`, className)} {...props}>\n {(title || subtitle) && (\n <header className="card__header">\n {title && <h2 className="card__title">{title}</h2>}\n {subtitle && <p className="card__subtitle">{subtitle}</p>}\n </header>\n )}\n\n {children && <div className="card__body">{children}</div>}\n </div>\n );\n};\n',
50
+ 'packages/components/card/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as CardStories from './card.stories';\n\n<Meta of={CardStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={CardStories.Default} />\n<Canvas of={CardStories.Muted} />\n<Canvas of={CardStories.Highlighted} />\n",
51
+ 'packages/components/card/index.test.ts': "import { describe, it, expect } from '@jest/globals';\nimport * as publicApi from './index.js';\n\ndescribe('card public API', () => {\n it('exports Card', () => {\n expect(publicApi.Card).toBeDefined();\n });\n});\n",
52
+ 'packages/components/card/index.ts': "export * from './card.js';\nexport type * from './types.js';\n",
53
+ 'packages/components/card/README.md': "# Card\n\n`Card` is a small layout primitive for profile-card UI surfaces.\n\n## Usage\n\n```tsx\nimport { Card } from './card.js';\n\nexport const Example = () => (\n <Card title=\"Profile\" subtitle=\"Calm architecture\">\n Content\n </Card>\n);\n```\n\n## Props\n\n```ts\nexport type CardProps = {\n title?: ReactNode;\n subtitle?: ReactNode;\n children?: ReactNode;\n variant?: 'default' | 'muted' | 'highlighted';\n} & ComponentProps<'div'>;\n```\n\n## Notes\n\n- SSR/SSG-safe.\n- Root element accepts regular `div` props.\n- Styles are local to the component.\n",
54
+ 'packages/components/card/styles.scss': '.card {\n border: 1px solid var(--profile-card-border-color, #e5e7eb);\n border-radius: var(--profile-card-radius, 1rem);\n background: var(--profile-card-background, #ffffff);\n box-shadow: var(--profile-card-shadow, 0 10px 30px rgb(15 23 42 / 8%));\n color: var(--profile-card-color, #111827);\n padding: var(--profile-card-space, 1rem);\n}\n\n.card--default {\n background: var(--profile-card-background, #ffffff);\n}\n\n.card--muted {\n background: var(--profile-card-muted-background, #f9fafb);\n}\n\n.card--highlighted {\n border-color: var(--profile-card-highlight-border-color, #93c5fd);\n background: var(--profile-card-highlight-background, #eff6ff);\n}\n\n.card__header {\n display: grid;\n gap: 0.25rem;\n margin-block-end: 0.75rem;\n}\n\n.card__title {\n margin: 0;\n font-size: 1rem;\n font-weight: 650;\n letter-spacing: 0;\n line-height: 1.3;\n}\n\n.card__subtitle {\n margin: 0;\n color: var(--profile-card-muted-color, #6b7280);\n font-size: 0.875rem;\n line-height: 1.4;\n}\n\n.card__body {\n display: grid;\n gap: 0.75rem;\n}\n',
55
+ 'packages/components/card/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Props for the Card component. */\nexport type CardProps = {\n title?: ReactNode;\n subtitle?: ReactNode;\n children?: ReactNode;\n variant?: 'default' | 'muted' | 'highlighted';\n} & ComponentProps<'div'>;\n\n/** Card component type. */\nexport type CardType = FC<CardProps>;\n",
56
+ 'packages/components/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Packages/Components\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
57
+ 'packages/components/icon-link/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as IconLinkStories from './icon-link.stories';\n\n<Meta of={IconLinkStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={IconLinkStories.Default} />\n<Canvas of={IconLinkStories.External} />\n",
58
+ 'packages/components/icon-link/icon-link.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { IconLink } from './icon-link.js';\n\nconst meta = {\n title: 'Components/IconLink',\n component: IconLink,\n parameters: { docs: { page: null } },\n args: { href: '#', label: 'GitHub', icon: 'GH' },\n} satisfies Meta<typeof IconLink>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const External: Story = {\n args: { href: 'https://example.com', label: 'Website', external: true },\n};\n",
59
+ 'packages/components/icon-link/icon-link.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { IconLink } from './icon-link.js';\n\ndescribe('IconLink', () => {\n it('renders readable link label', () => {\n render(<IconLink href=\"/github\" label=\"GitHub\" icon={<span>G</span>} />);\n\n expect(screen.getByRole('link', { name: 'GitHub' })).toBeDefined();\n });\n\n it('adds external link attributes', () => {\n render(<IconLink external href=\"https://example.com\" label=\"Website\" />);\n\n const link = screen.getByRole('link', { name: 'Website' });\n\n expect(link.getAttribute('target')).toBe('_blank');\n expect(link.getAttribute('rel')).toBe('noreferrer');\n });\n});\n",
60
+ 'packages/components/icon-link/icon-link.tsx': "import { cn } from '@vyriy/cn';\n\nimport type { IconLinkType } from './types.js';\n\n/** Renders a compact accessible link with an optional decorative icon. */\nexport const IconLink: IconLinkType = ({ href, label, icon, external = false, className, ...props }) => {\n return (\n <a\n className={cn('icon-link', className)}\n href={href}\n target={external ? '_blank' : props.target}\n rel={external ? 'noreferrer' : props.rel}\n {...props}\n >\n {icon && (\n <span className=\"icon-link__icon\" aria-hidden=\"true\">\n {icon}\n </span>\n )}\n <span className=\"icon-link__label\">{label}</span>\n </a>\n );\n};\n",
61
+ 'packages/components/icon-link/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('icon-link public API', () => {\n it('exports IconLink', () => {\n expect(publicApi.IconLink).toBeDefined();\n });\n});\n",
62
+ 'packages/components/icon-link/index.ts': "export * from './icon-link.js';\nexport type * from './types.js';\n",
63
+ 'packages/components/icon-link/README.md': '# IconLink\n\n`IconLink` renders a compact profile link with optional decorative icon content.\n\n## Usage\n\n```tsx\nimport { IconLink } from \'./icon-link.js\';\n\nexport const Example = () => <IconLink href="/github" label="GitHub" />;\n```\n\n## Props\n\n```ts\nexport type IconLinkProps = {\n href: string;\n label: string;\n icon?: ReactNode;\n external?: boolean;\n} & ComponentProps<\'a\'>;\n```\n\n## Accessibility\n\nThe label remains readable link text. Optional icon content is decorative.\n\n## Notes\n\n- SSR/SSG-safe.\n- External links receive safe target and rel attributes.\n- Shared SCSS styles.\n',
64
+ 'packages/components/icon-link/styles.scss': '.icon-link {\n display: inline-flex;\n align-items: center;\n gap: 0.4rem;\n min-height: 2rem;\n border-radius: 0.5rem;\n color: var(--profile-card-color, #111827);\n font-size: 0.875rem;\n font-weight: 600;\n letter-spacing: 0;\n line-height: 1.2;\n text-decoration: none;\n}\n\n.icon-link:hover {\n color: #1d4ed8;\n}\n\n.icon-link:focus-visible {\n outline: none;\n box-shadow: var(--profile-card-focus-ring, 0 0 0 3px rgb(59 130 246 / 30%));\n}\n\n.icon-link__icon {\n display: inline-grid;\n width: 1rem;\n height: 1rem;\n place-items: center;\n}\n\n.icon-link__label {\n overflow-wrap: anywhere;\n}\n',
65
+ 'packages/components/icon-link/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Props for the IconLink component. */\nexport type IconLinkProps = {\n href: string;\n label: string;\n icon?: ReactNode;\n external?: boolean;\n} & ComponentProps<'a'>;\n\n/** IconLink component type. */\nexport type IconLinkType = FC<IconLinkProps>;\n",
66
+ 'packages/components/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('public API', () => {\n it('exports public components', () => {\n expect(publicApi.Avatar).toBeDefined();\n expect(publicApi.Badge).toBeDefined();\n expect(publicApi.ButtonLink).toBeDefined();\n expect(publicApi.Card).toBeDefined();\n expect(publicApi.IconLink).toBeDefined();\n expect(publicApi.ProfileCard).toBeDefined();\n expect(publicApi.ProfileDetails).toBeDefined();\n expect(publicApi.ProfileHeader).toBeDefined();\n expect(publicApi.ProfileLinks).toBeDefined();\n expect(publicApi.ProfileMeta).toBeDefined();\n expect(publicApi.ProfileTags).toBeDefined();\n });\n});\n",
67
+ 'packages/components/index.ts': "export * from './avatar/index.js';\nexport * from './badge/index.js';\nexport * from './button-link/index.js';\nexport * from './card/index.js';\nexport * from './icon-link/index.js';\nexport * from './profile-card/index.js';\nexport * from './profile-details/index.js';\nexport * from './profile-header/index.js';\nexport * from './profile-links/index.js';\nexport * from './profile-meta/index.js';\nexport * from './profile-tags/index.js';\n",
68
+ 'packages/components/profile-card/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ProfileCardStories from './profile-card.stories';\n\n<Meta of={ProfileCardStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ProfileCardStories.Default} />\n<Canvas of={ProfileCardStories.Minimal} />\n",
69
+ 'packages/components/profile-card/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('profile-card public API', () => {\n it('exports ProfileCard', () => {\n expect(publicApi.ProfileCard).toBeDefined();\n });\n});\n",
70
+ 'packages/components/profile-card/index.ts': "export { ProfileCard } from './profile-card.js';\nexport type {\n ProfileCard as ProfileCardData,\n ProfileCardLink,\n ProfileCardMetaItem,\n ProfileCardProps,\n ProfileCardType,\n} from './types.js';\n",
71
+ 'packages/components/profile-card/profile-card.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ProfileCard } from './profile-card.js';\n\nconst meta = {\n title: 'Components/ProfileCard',\n component: ProfileCard,\n parameters: { docs: { page: null } },\n args: {\n name: 'Developer',\n title: 'Senior IT Professional',\n description: 'Building calm architecture for cloud-ready applications.',\n tags: [\n 'React',\n 'TypeScript',\n 'AWS',\n 'Serverless',\n 'Vyriy',\n ],\n meta: [\n { label: 'Project', value: 'Vyriy' },\n { label: 'Focus', value: 'Calm architecture' },\n ],\n links: [\n { label: 'GitHub', href: 'https://github.com', external: true },\n { label: 'Website', href: 'https://vyriy.dev', external: true },\n ],\n children: <p>Reusable profile UI for MFE preset examples.</p>,\n },\n} satisfies Meta<typeof ProfileCard>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Minimal: Story = {\n args: {\n title: undefined,\n description: undefined,\n tags: [],\n meta: [],\n links: [],\n children: undefined,\n },\n};\n",
72
+ 'packages/components/profile-card/profile-card.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ProfileCard } from './profile-card.js';\n\ndescribe('ProfileCard', () => {\n it('renders composed profile content', () => {\n render(\n <ProfileCard\n name=\"Developer\"\n title=\"Senior IT Professional\"\n description=\"Building calm architecture.\"\n tags={['React', 'TypeScript']}\n meta={[{ label: 'Project', value: 'Vyriy' }]}\n links={[{ href: 'https://vyriy.dev', label: 'Website', external: true }]}\n >\n Custom summary\n </ProfileCard>,\n );\n\n expect(screen.getByRole('article')).toBeDefined();\n expect(screen.getByRole('heading', { name: 'Developer' })).toBeDefined();\n expect(screen.getByText('Senior IT Professional')).toBeDefined();\n expect(screen.getByText('React')).toBeDefined();\n expect(screen.getByText('Vyriy')).toBeDefined();\n expect(screen.getByText('Custom summary')).toBeDefined();\n expect(screen.getByRole('link', { name: 'Website' })).toBeDefined();\n });\n\n it('passes article props to the root element', () => {\n render(<ProfileCard name=\"Developer\" data-testid=\"profile-card\" />);\n\n expect(screen.getByTestId('profile-card')).toBeDefined();\n });\n});\n",
73
+ 'packages/components/profile-card/profile-card.tsx': "import { cn } from '@vyriy/cn';\n\nimport { Card } from '../card/index.js';\nimport { ProfileDetails } from '../profile-details/index.js';\nimport { ProfileHeader } from '../profile-header/index.js';\nimport { ProfileLinks } from '../profile-links/index.js';\nimport { ProfileMeta } from '../profile-meta/index.js';\nimport { ProfileTags } from '../profile-tags/index.js';\nimport type { ProfileCardType } from './types.js';\n\n/** Composes the profile-card primitives into one semantic article. */\nexport const ProfileCard: ProfileCardType = ({\n name,\n title,\n description,\n avatarUrl,\n tags = [],\n meta = [],\n links = [],\n children,\n className,\n ...props\n}) => {\n return (\n <article className={cn('profile-card', className)} {...props}>\n <Card>\n <div className=\"profile-card__content\">\n <ProfileHeader avatarUrl={avatarUrl} description={description} name={name} title={title} />\n <ProfileMeta items={meta} />\n <ProfileTags tags={tags} tone=\"blue\" />\n <ProfileDetails>{children}</ProfileDetails>\n <ProfileLinks links={links} />\n </div>\n </Card>\n </article>\n );\n};\n",
74
+ 'packages/components/profile-card/README.md': "# ProfileCard\n\n`ProfileCard` composes the profile-card primitives into one semantic article.\n\n## Usage\n\n```tsx\nimport { ProfileCard } from './profile-card.js';\n\nexport const Example = () => (\n <ProfileCard\n name=\"Developer\"\n title=\"Senior IT Professional\"\n description=\"Building calm architecture for cloud-ready applications.\"\n tags={['React', 'TypeScript', 'Vyriy']}\n meta={[{ label: 'Project', value: 'Vyriy' }]}\n links={[{ href: 'https://vyriy.dev', label: 'Website', external: true }]}\n />\n);\n```\n\n## Props\n\n```ts\nexport type ProfileCardProps = {\n name: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n tags?: string[];\n meta?: ProfileCardMetaItem[];\n links?: ProfileCardLink[];\n children?: ReactNode;\n} & ComponentProps<'article'>;\n```\n\n## Accessibility\n\nThe root element is an `<article>`. Nested metadata, tag, and link sections use semantic markup.\n\n## Notes\n\n- SSR/SSG-safe.\n- Deterministic output.\n- Shared SCSS styles.\n",
75
+ 'packages/components/profile-card/styles.scss': '.profile-card {\n max-width: 28rem;\n color: var(--profile-card-color, #111827);\n}\n\n.profile-card__content {\n display: grid;\n gap: 1rem;\n}\n',
76
+ 'packages/components/profile-card/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Link item rendered in a profile card. */\nexport type ProfileCardLink = {\n href: string;\n label: string;\n external?: boolean;\n};\n\n/** Metadata item rendered in a profile card. */\nexport type ProfileCardMetaItem = {\n label: string;\n value: ReactNode;\n};\n\n/** Data model accepted by the ProfileCard component and API renderers. */\nexport type ProfileCard = {\n name: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n tags?: string[];\n meta?: ProfileCardMetaItem[];\n links?: ProfileCardLink[];\n children?: ReactNode;\n};\n\n/** Props for the ProfileCard component. */\nexport type ProfileCardProps = ProfileCard & ComponentProps<'article'>;\n\n/** ProfileCard component type. */\nexport type ProfileCardType = FC<ProfileCardProps>;\n",
77
+ 'packages/components/profile-details/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ProfileDetailsStories from './profile-details.stories';\n\n<Meta of={ProfileDetailsStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ProfileDetailsStories.Default} />\n",
78
+ 'packages/components/profile-details/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('profile-details public API', () => {\n it('exports ProfileDetails', () => {\n expect(publicApi.ProfileDetails).toBeDefined();\n });\n});\n",
79
+ 'packages/components/profile-details/index.ts': "export * from './profile-details.js';\nexport type * from './types.js';\n",
80
+ 'packages/components/profile-details/profile-details.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ProfileDetails } from './profile-details.js';\n\nconst meta = {\n title: 'Components/ProfileDetails',\n component: ProfileDetails,\n parameters: { docs: { page: null } },\n args: { children: <p>Builds typed, deployable systems with calm boundaries.</p> },\n} satisfies Meta<typeof ProfileDetails>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n",
81
+ 'packages/components/profile-details/profile-details.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ProfileDetails } from './profile-details.js';\n\ndescribe('ProfileDetails', () => {\n it('renders body content', () => {\n render(\n <ProfileDetails>\n <p>Profile summary</p>\n </ProfileDetails>,\n );\n\n expect(screen.getByText('Profile summary')).toBeDefined();\n });\n\n it('does not render an empty wrapper', () => {\n const { container } = render(<ProfileDetails />);\n\n expect(container.firstChild).toBeNull();\n });\n});\n",
82
+ 'packages/components/profile-details/profile-details.tsx': "import { cn } from '@vyriy/cn';\n\nimport type { ProfileDetailsType } from './types.js';\n\n/** Renders optional long-form profile details. */\nexport const ProfileDetails: ProfileDetailsType = ({ children, className, ...props }) => {\n if (!children) {\n return null;\n }\n\n return (\n <section className={cn('profile-details', className)} {...props}>\n {children}\n </section>\n );\n};\n",
83
+ 'packages/components/profile-details/README.md': "# ProfileDetails\n\n`ProfileDetails` renders optional profile summary, skills, or custom body content.\n\n## Usage\n\n```tsx\nimport { ProfileDetails } from './profile-details.js';\n\nexport const Example = () => <ProfileDetails>Profile summary</ProfileDetails>;\n```\n\n## Props\n\n```ts\nexport type ProfileDetailsProps = {\n children?: ReactNode;\n} & ComponentProps<'section'>;\n```\n\n## Accessibility\n\nThe component renders a semantic `<section>` when content exists.\n\n## Notes\n\n- Empty content renders `null`.\n- SSR/SSG-safe.\n- Shared SCSS styles.\n",
84
+ 'packages/components/profile-details/styles.scss': '.profile-details {\n color: var(--profile-card-muted-color, #6b7280);\n font-size: 0.9rem;\n line-height: 1.55;\n}\n\n.profile-details > :first-child {\n margin-block-start: 0;\n}\n\n.profile-details > :last-child {\n margin-block-end: 0;\n}\n',
85
+ 'packages/components/profile-details/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Props for the ProfileDetails component. */\nexport type ProfileDetailsProps = {\n children?: ReactNode;\n} & ComponentProps<'section'>;\n\n/** ProfileDetails component type. */\nexport type ProfileDetailsType = FC<ProfileDetailsProps>;\n",
86
+ 'packages/components/profile-header/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ProfileHeaderStories from './profile-header.stories';\n\n<Meta of={ProfileHeaderStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ProfileHeaderStories.Default} />\n<Canvas of={ProfileHeaderStories.WithAvatar} />\n",
87
+ 'packages/components/profile-header/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('profile-header public API', () => {\n it('exports ProfileHeader', () => {\n expect(publicApi.ProfileHeader).toBeDefined();\n });\n});\n",
88
+ 'packages/components/profile-header/index.ts': "export * from './profile-header.js';\nexport type * from './types.js';\n",
89
+ 'packages/components/profile-header/profile-header.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ProfileHeader } from './profile-header.js';\n\nconst meta = {\n title: 'Components/ProfileHeader',\n component: ProfileHeader,\n parameters: { docs: { page: null } },\n args: {\n name: 'Developer',\n title: 'Senior IT Professional',\n description: 'Building calm architecture for cloud-ready applications.',\n },\n} satisfies Meta<typeof ProfileHeader>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const WithAvatar: Story = {\n args: {\n avatarUrl: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=128&q=80',\n },\n};\n",
90
+ 'packages/components/profile-header/profile-header.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ProfileHeader } from './profile-header.js';\n\ndescribe('ProfileHeader', () => {\n it('renders name, title, and description', () => {\n render(\n <ProfileHeader\n name=\"Ada Lovelace\"\n title=\"Computing pioneer\"\n description=\"Wrote early notes about general computation.\"\n />,\n );\n\n expect(screen.getByRole('heading', { name: 'Ada Lovelace' })).toBeDefined();\n expect(screen.getByText('Computing pioneer')).toBeDefined();\n expect(screen.getByText('Wrote early notes about general computation.')).toBeDefined();\n });\n\n it('passes header props to the root element', () => {\n render(<ProfileHeader name=\"Ada Lovelace\" data-testid=\"profile-header\" />);\n\n expect(screen.getByTestId('profile-header')).toBeDefined();\n });\n});\n",
91
+ 'packages/components/profile-header/profile-header.tsx': 'import { cn } from \'@vyriy/cn\';\n\nimport { Avatar } from \'../avatar/index.js\';\nimport type { ProfileHeaderType } from \'./types.js\';\n\n/** Renders profile identity, title, description, and avatar. */\nexport const ProfileHeader: ProfileHeaderType = ({ name, title, description, avatarUrl, className, ...props }) => {\n return (\n <header className={cn(\'profile-header\', className)} {...props}>\n <Avatar src={avatarUrl} name={name} alt={`${name} avatar`} size="lg" />\n <div className="profile-header__content">\n <h2 className="profile-header__name">{name}</h2>\n {title && <p className="profile-header__title">{title}</p>}\n {description && <p className="profile-header__description">{description}</p>}\n </div>\n </header>\n );\n};\n',
92
+ 'packages/components/profile-header/README.md': '# ProfileHeader\n\n`ProfileHeader` composes an avatar, name, title, and short description.\n\n## Usage\n\n```tsx\nimport { ProfileHeader } from \'./profile-header.js\';\n\nexport const Example = () => <ProfileHeader name="Developer" title="Senior IT Professional" />;\n```\n\n## Props\n\n```ts\nexport type ProfileHeaderProps = {\n name: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n} & ComponentProps<\'header\'>;\n```\n\n## Accessibility\n\nThe root element is a semantic `<header>`, and avatar alt text is derived from the profile name.\n\n## Notes\n\n- SSR/SSG-safe.\n- Deterministic rendering.\n- Shared SCSS styles.\n',
93
+ 'packages/components/profile-header/styles.scss': '.profile-header {\n display: flex;\n align-items: flex-start;\n gap: 0.9rem;\n min-width: 0;\n}\n\n.profile-header__content {\n display: grid;\n min-width: 0;\n gap: 0.25rem;\n}\n\n.profile-header__name {\n margin: 0;\n color: var(--profile-card-color, #111827);\n font-size: 1.2rem;\n font-weight: 750;\n letter-spacing: 0;\n line-height: 1.2;\n}\n\n.profile-header__title {\n margin: 0;\n color: var(--profile-card-color, #111827);\n font-size: 0.925rem;\n font-weight: 650;\n line-height: 1.35;\n}\n\n.profile-header__description {\n margin: 0.25rem 0 0;\n color: var(--profile-card-muted-color, #6b7280);\n font-size: 0.9rem;\n line-height: 1.45;\n}\n',
94
+ 'packages/components/profile-header/types.ts': "import type { ComponentProps, FC } from 'react';\n\n/** Props for the ProfileHeader component. */\nexport type ProfileHeaderProps = {\n name: string;\n title?: string;\n description?: string;\n avatarUrl?: string;\n} & ComponentProps<'header'>;\n\n/** ProfileHeader component type. */\nexport type ProfileHeaderType = FC<ProfileHeaderProps>;\n",
95
+ 'packages/components/profile-links/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ProfileLinksStories from './profile-links.stories';\n\n<Meta of={ProfileLinksStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ProfileLinksStories.Default} />\n<Canvas of={ProfileLinksStories.Buttons} />\n",
96
+ 'packages/components/profile-links/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('profile-links public API', () => {\n it('exports ProfileLinks', () => {\n expect(publicApi.ProfileLinks).toBeDefined();\n });\n});\n",
97
+ 'packages/components/profile-links/index.ts': "export * from './profile-links.js';\nexport type * from './types.js';\n",
98
+ 'packages/components/profile-links/profile-links.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ProfileLinks } from './profile-links.js';\n\nconst meta = {\n title: 'Components/ProfileLinks',\n component: ProfileLinks,\n parameters: { docs: { page: null } },\n args: {\n links: [\n { label: 'GitHub', href: 'https://github.com', external: true },\n { label: 'Website', href: 'https://vyriy.dev', external: true },\n ],\n },\n} satisfies Meta<typeof ProfileLinks>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Buttons: Story = { args: { variant: 'buttons' } };\n",
99
+ 'packages/components/profile-links/profile-links.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ProfileLinks } from './profile-links.js';\n\ndescribe('ProfileLinks', () => {\n it('renders profile links in navigation', () => {\n render(<ProfileLinks links={[{ href: '/github', label: 'GitHub' }]} />);\n\n expect(screen.getByRole('navigation', { name: 'Profile links' })).toBeDefined();\n expect(screen.getByRole('link', { name: 'GitHub' })).toBeDefined();\n });\n\n it('renders external button links', () => {\n render(\n <ProfileLinks links={[{ href: 'https://example.com', label: 'Website', external: true }]} variant=\"buttons\" />,\n );\n\n const link = screen.getByRole('link', { name: 'Website' });\n\n expect(link.getAttribute('target')).toBe('_blank');\n expect(link.getAttribute('rel')).toBe('noreferrer');\n });\n\n it('does not render an empty wrapper', () => {\n const { container } = render(<ProfileLinks links={[]} />);\n\n expect(container.firstChild).toBeNull();\n });\n});\n",
100
+ 'packages/components/profile-links/profile-links.tsx': "import { cn } from '@vyriy/cn';\n\nimport { ButtonLink } from '../button-link/index.js';\nimport { IconLink } from '../icon-link/index.js';\nimport type { ProfileLinksType } from './types.js';\n\n/** Renders profile links as icon links or button-style links. */\nexport const ProfileLinks: ProfileLinksType = ({\n links,\n variant = 'icons',\n className,\n 'aria-label': ariaLabel = 'Profile links',\n ...props\n}) => {\n if (links.length === 0) {\n return null;\n }\n\n return (\n <nav className={cn('profile-links', `profile-links--${variant}`, className)} aria-label={ariaLabel} {...props}>\n {links.map((link) =>\n variant === 'buttons' ? (\n <ButtonLink external={link.external} href={link.href} key={link.href} variant=\"secondary\">\n {link.label}\n </ButtonLink>\n ) : (\n <IconLink external={link.external} href={link.href} key={link.href} label={link.label} />\n ),\n )}\n </nav>\n );\n};\n",
101
+ 'packages/components/profile-links/README.md': "# ProfileLinks\n\n`ProfileLinks` renders profile links as compact text links or button links.\n\n## Usage\n\n```tsx\nimport { ProfileLinks } from './profile-links.js';\n\nexport const Example = () => <ProfileLinks links={[{ href: '/github', label: 'GitHub' }]} />;\n```\n\n## Props\n\n```ts\nexport type ProfileLink = {\n href: string;\n label: string;\n external?: boolean;\n};\n\nexport type ProfileLinksProps = {\n links: ProfileLink[];\n variant?: 'icons' | 'buttons';\n} & ComponentProps<'nav'>;\n```\n\n## Accessibility\n\nThe component renders `<nav aria-label=\"Profile links\">` by default.\n\n## Notes\n\n- Empty link arrays render `null`.\n- External links receive safe target and rel attributes.\n- SSR/SSG-safe.\n",
102
+ 'packages/components/profile-links/styles.scss': '.profile-links {\n display: flex;\n flex-wrap: wrap;\n gap: 0.55rem;\n}\n\n.profile-links--icons {\n align-items: center;\n}\n\n.profile-links--buttons {\n align-items: stretch;\n}\n',
103
+ 'packages/components/profile-links/types.ts': "import type { ComponentProps, FC } from 'react';\n\n/** Link item rendered by ProfileLinks. */\nexport type ProfileLink = {\n href: string;\n label: string;\n external?: boolean;\n};\n\n/** Props for the ProfileLinks component. */\nexport type ProfileLinksProps = {\n links: ProfileLink[];\n variant?: 'icons' | 'buttons';\n} & ComponentProps<'nav'>;\n\n/** ProfileLinks component type. */\nexport type ProfileLinksType = FC<ProfileLinksProps>;\n",
104
+ 'packages/components/profile-meta/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ProfileMetaStories from './profile-meta.stories';\n\n<Meta of={ProfileMetaStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ProfileMetaStories.Default} />\n",
105
+ 'packages/components/profile-meta/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('profile-meta public API', () => {\n it('exports ProfileMeta', () => {\n expect(publicApi.ProfileMeta).toBeDefined();\n });\n});\n",
106
+ 'packages/components/profile-meta/index.ts': "export * from './profile-meta.js';\nexport type * from './types.js';\n",
107
+ 'packages/components/profile-meta/profile-meta.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ProfileMeta } from './profile-meta.js';\n\nconst meta = {\n title: 'Components/ProfileMeta',\n component: ProfileMeta,\n parameters: { docs: { page: null } },\n args: {\n items: [\n { label: 'Project', value: 'Vyriy' },\n { label: 'Focus', value: 'Calm architecture' },\n ],\n },\n} satisfies Meta<typeof ProfileMeta>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n",
108
+ 'packages/components/profile-meta/profile-meta.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ProfileMeta } from './profile-meta.js';\n\ndescribe('ProfileMeta', () => {\n it('renders metadata in a description list', () => {\n render(<ProfileMeta items={[{ label: 'Location', value: 'Kyiv' }]} />);\n\n expect(screen.getByText('Location')).toBeDefined();\n expect(screen.getByText('Kyiv')).toBeDefined();\n });\n\n it('does not render an empty wrapper', () => {\n const { container } = render(<ProfileMeta items={[]} />);\n\n expect(container.firstChild).toBeNull();\n });\n});\n",
109
+ 'packages/components/profile-meta/profile-meta.tsx': 'import { cn } from \'@vyriy/cn\';\n\nimport type { ProfileMetaType } from \'./types.js\';\n\n/** Renders profile metadata as a semantic description list. */\nexport const ProfileMeta: ProfileMetaType = ({ items, className, ...props }) => {\n if (items.length === 0) {\n return null;\n }\n\n return (\n <dl className={cn(\'profile-meta\', className)} {...props}>\n {items.map((item) => (\n <div className="profile-meta__item" key={item.label}>\n <dt className="profile-meta__label">{item.label}</dt>\n <dd className="profile-meta__value">{item.value}</dd>\n </div>\n ))}\n </dl>\n );\n};\n',
110
+ 'packages/components/profile-meta/README.md': "# ProfileMeta\n\n`ProfileMeta` renders compact metadata rows for location, company, availability, and stack.\n\n## Usage\n\n```tsx\nimport { ProfileMeta } from './profile-meta.js';\n\nexport const Example = () => <ProfileMeta items={[{ label: 'Project', value: 'Vyriy' }]} />;\n```\n\n## Props\n\n```ts\nexport type ProfileMetaItem = {\n label: string;\n value: ReactNode;\n};\n\nexport type ProfileMetaProps = {\n items: ProfileMetaItem[];\n} & ComponentProps<'dl'>;\n```\n\n## Accessibility\n\nThe component uses semantic `<dl>`, `<dt>`, and `<dd>` markup.\n\n## Notes\n\n- Empty item arrays render `null`.\n- SSR/SSG-safe.\n- Shared SCSS styles.\n",
111
+ 'packages/components/profile-meta/styles.scss': '.profile-meta {\n display: grid;\n gap: 0.55rem;\n margin: 0;\n}\n\n.profile-meta__item {\n display: grid;\n grid-template-columns: minmax(5rem, 0.45fr) 1fr;\n gap: 0.75rem;\n min-width: 0;\n}\n\n.profile-meta__label {\n color: var(--profile-card-muted-color, #6b7280);\n font-size: 0.78rem;\n font-weight: 650;\n line-height: 1.35;\n}\n\n.profile-meta__value {\n min-width: 0;\n margin: 0;\n color: var(--profile-card-color, #111827);\n font-size: 0.875rem;\n line-height: 1.35;\n overflow-wrap: anywhere;\n}\n',
112
+ 'packages/components/profile-meta/types.ts': "import type { ComponentProps, FC, ReactNode } from 'react';\n\n/** Metadata item rendered by ProfileMeta. */\nexport type ProfileMetaItem = {\n label: string;\n value: ReactNode;\n};\n\n/** Props for the ProfileMeta component. */\nexport type ProfileMetaProps = {\n items: ProfileMetaItem[];\n} & ComponentProps<'dl'>;\n\n/** ProfileMeta component type. */\nexport type ProfileMetaType = FC<ProfileMetaProps>;\n",
113
+ 'packages/components/profile-tags/doc.mdx': "import { Meta, Markdown, Canvas } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\nimport * as ProfileTagsStories from './profile-tags.stories';\n\n<Meta of={ProfileTagsStories} />\n\n<Markdown>{ReadMe}</Markdown>\n\n## Examples\n\n<Canvas of={ProfileTagsStories.Default} />\n<Canvas of={ProfileTagsStories.Neutral} />\n",
114
+ 'packages/components/profile-tags/index.test.ts': "import { describe, it, expect } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('profile-tags public API', () => {\n it('exports ProfileTags', () => {\n expect(publicApi.ProfileTags).toBeDefined();\n });\n});\n",
115
+ 'packages/components/profile-tags/index.ts': "export * from './profile-tags.js';\nexport type * from './types.js';\n",
116
+ 'packages/components/profile-tags/profile-tags.stories.tsx': "import type { Meta, StoryObj } from '@storybook/react-webpack5';\n\nimport { ProfileTags } from './profile-tags.js';\n\nconst meta = {\n title: 'Components/ProfileTags',\n component: ProfileTags,\n parameters: { docs: { page: null } },\n args: {\n tags: [\n 'React',\n 'TypeScript',\n 'AWS',\n 'Serverless',\n ],\n tone: 'blue',\n },\n} satisfies Meta<typeof ProfileTags>;\n\nexport default meta;\n\ntype Story = StoryObj<typeof meta>;\n\nexport const Default: Story = {};\n\nexport const Neutral: Story = { args: { tone: 'neutral' } };\n",
117
+ 'packages/components/profile-tags/profile-tags.test.tsx': "import { describe, it, expect } from '@jest/globals';\nimport { render, screen } from '@testing-library/react';\n\nimport { ProfileTags } from './profile-tags.js';\n\ndescribe('ProfileTags', () => {\n it('renders tags as a list', () => {\n render(<ProfileTags tags={['React', 'TypeScript']} />);\n\n expect(screen.getByRole('list')).toBeDefined();\n expect(screen.getByText('React')).toBeDefined();\n expect(screen.getByText('TypeScript')).toBeDefined();\n });\n\n it('does not render an empty wrapper', () => {\n const { container } = render(<ProfileTags tags={[]} />);\n\n expect(container.firstChild).toBeNull();\n });\n});\n",
118
+ 'packages/components/profile-tags/profile-tags.tsx': "import { cn } from '@vyriy/cn';\n\nimport { Badge } from '../badge/index.js';\nimport type { ProfileTagsType } from './types.js';\n\n/** Renders profile tags as a semantic list of badges. */\nexport const ProfileTags: ProfileTagsType = ({ tags, tone = 'neutral', className, ...props }) => {\n if (tags.length === 0) {\n return null;\n }\n\n return (\n <ul className={cn('profile-tags', className)} {...props}>\n {tags.map((tag) => (\n <li className=\"profile-tags__item\" key={tag}>\n <Badge tone={tone}>{tag}</Badge>\n </li>\n ))}\n </ul>\n );\n};\n",
119
+ 'packages/components/profile-tags/README.md': "# ProfileTags\n\n`ProfileTags` renders a list of profile tags using `Badge`.\n\n## Usage\n\n```tsx\nimport { ProfileTags } from './profile-tags.js';\n\nexport const Example = () => <ProfileTags tags={['React', 'TypeScript']} />;\n```\n\n## Props\n\n```ts\nexport type ProfileTagsProps = {\n tags: string[];\n tone?: BadgeProps['tone'];\n} & ComponentProps<'ul'>;\n```\n\n## Accessibility\n\nTags are rendered as a semantic `<ul>` with one `<li>` per tag.\n\n## Notes\n\n- Empty tag arrays render `null`.\n- SSR/SSG-safe.\n- Shared SCSS styles.\n",
120
+ 'packages/components/profile-tags/styles.scss': '.profile-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 0.4rem;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.profile-tags__item {\n display: inline-flex;\n}\n',
121
+ 'packages/components/profile-tags/types.ts': "import type { ComponentProps, FC } from 'react';\n\nimport type { BadgeProps } from '../badge/index.js';\n\n/** Props for the ProfileTags component. */\nexport type ProfileTagsProps = {\n tags: string[];\n tone?: BadgeProps['tone'];\n} & ComponentProps<'ul'>;\n\n/** ProfileTags component type. */\nexport type ProfileTagsType = FC<ProfileTagsProps>;\n",
122
+ 'packages/components/README.md': '# @p/components\n\nReusable React primitives for the profile-card microfrontend.\n\n## Exports\n\nThe package exports the public profile-card component set:\n\n- `Avatar`\n- `Badge`\n- `ButtonLink`\n- `Card`\n- `IconLink`\n- `ProfileCard`\n- `ProfileDetails`\n- `ProfileHeader`\n- `ProfileLinks`\n- `ProfileMeta`\n- `ProfileTags`\n\nEach component has a focused README, Storybook docs, stories, and unit tests in its own folder.\n',
123
+ 'packages/components/styles.d.ts': "declare module '*.scss';\ndeclare module '*.scss?inline' {\n const css: string;\n export default css;\n}\ndeclare module '*.svg' {\n const src: string;\n export default src;\n}\n",
124
+ 'packages/components/styles.scss': "@use './avatar/styles' as avatar;\n@use './badge/styles' as badge;\n@use './button-link/styles' as button-link;\n@use './card/styles' as card;\n@use './icon-link/styles' as icon-link;\n@use './profile-card/styles' as profile-card;\n@use './profile-details/styles' as profile-details;\n@use './profile-header/styles' as profile-header;\n@use './profile-links/styles' as profile-links;\n@use './profile-meta/styles' as profile-meta;\n@use './profile-tags/styles' as profile-tags;\n",
125
+ 'packages/env/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Packages/Env\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
126
+ 'packages/env/env.test.ts': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport { getApi, getCdn, getMode, getTag, getUi } from './env.js';\n\ndescribe('env getters', () => {\n afterEach(() => {\n delete process.env.API;\n delete process.env.CDN;\n delete process.env.MODE;\n delete process.env.TAG;\n delete process.env.UI;\n });\n\n it('reads required environment values', () => {\n process.env.API = 'http://localhost:3000';\n process.env.CDN = 'http://localhost:3001';\n process.env.MODE = 'open';\n process.env.TAG = 'vyriy-profile-card';\n process.env.UI = 'http://localhost:3002';\n\n expect(getApi()).toBe('http://localhost:3000');\n expect(getCdn()).toBe('http://localhost:3001');\n expect(getMode()).toBe('open');\n expect(getTag()).toBe('vyriy-profile-card');\n expect(getUi()).toBe('http://localhost:3002');\n });\n\n it('throws when a required environment value is missing', () => {\n expect(() => getUi()).toThrow('Environment variable UI is not defined!');\n });\n});\n",
127
+ 'packages/env/env.ts': "import type { GetEnv, GetMode } from './types.js';\n\nconst getEnv = (name: string, value: string | undefined): string => {\n if (value === undefined) {\n throw new Error(`Environment variable ${name} is not defined!`);\n }\n\n return value;\n};\n\n/** Reads the configured custom element tag name. */\nexport const getTag: GetEnv = () => getEnv('TAG', process.env.TAG);\n\n/** Reads the configured shadow DOM mode. */\nexport const getMode: GetMode = () => getEnv('MODE', process.env.MODE) as ReturnType<GetMode>;\n\n/** Reads the API origin used for server endpoints. */\nexport const getApi: GetEnv = () => getEnv('API', process.env.API);\n\n/** Reads the CDN origin used for static assets. */\nexport const getCdn: GetEnv = () => getEnv('CDN', process.env.CDN);\n\n/** Reads the UI origin used for browser assets. */\nexport const getUi: GetEnv = () => getEnv('UI', process.env.UI);\n",
128
+ 'packages/env/index.test.ts': "import { afterEach, describe, expect, it } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\nconst ENV_NAMES = [\n 'TAG',\n 'MODE',\n 'API',\n 'CDN',\n 'UI',\n] as const;\n\nconst clearEnv = () => {\n for (const name of ENV_NAMES) {\n delete process.env[name];\n }\n};\n\ndescribe('env public API', () => {\n afterEach(() => {\n clearEnv();\n });\n\n it('exports env getters', () => {\n expect(publicApi.getTag).toBeDefined();\n expect(publicApi.getMode).toBeDefined();\n expect(publicApi.getApi).toBeDefined();\n expect(publicApi.getCdn).toBeDefined();\n expect(publicApi.getUi).toBeDefined();\n });\n\n it('reads environment variables by public getter name', () => {\n process.env.TAG = 'vyriy-profile-card';\n process.env.MODE = 'open';\n process.env.API = 'http://localhost:3000';\n process.env.CDN = 'http://localhost:3001';\n process.env.UI = 'http://localhost:3002';\n\n expect(publicApi.getTag()).toBe('vyriy-profile-card');\n expect(publicApi.getMode()).toBe('open');\n expect(publicApi.getApi()).toBe('http://localhost:3000');\n expect(publicApi.getCdn()).toBe('http://localhost:3001');\n expect(publicApi.getUi()).toBe('http://localhost:3002');\n });\n\n it('throws when a required environment variable is missing', () => {\n clearEnv();\n\n expect(() => publicApi.getApi()).toThrow('Environment variable API is not defined!');\n });\n});\n",
129
+ 'packages/env/index.ts': "export * from './env.js';\nexport type * from './types.js';\n",
130
+ 'packages/env/package.json': '{\n "name": "@p/env",\n "type": "module",\n "private": true\n}\n',
131
+ 'packages/env/README.md': '# @p/env\n\nRequired environment readers shared by API and UI workspaces.\n\n## Exports\n\n- `getTag()` reads `TAG`.\n- `getMode()` reads `MODE`.\n- `getApi()` reads `API`.\n- `getCdn()` reads `CDN`.\n- `getUi()` reads `UI`.\n\nEach getter throws when its environment variable is missing.\n',
132
+ 'packages/env/types.ts': "/** Reads a required string environment variable. */\nexport type GetEnv = () => string | never;\n\n/** Reads the required declarative shadow DOM mode. */\nexport type GetMode = () => 'open' | 'closed' | never;\n",
133
+ 'packages/event/constants.test.ts': "import { describe, expect, it } from '@jest/globals';\n\nimport { PROFILE_CARD_ANALYTICS_EVENT_NAME, PROFILE_CARD_SELECT_EVENT_NAME } from './constants.js';\n\ndescribe('event constants', () => {\n it('matches the manifest event names', () => {\n expect(PROFILE_CARD_ANALYTICS_EVENT_NAME).toBe('openmfe.analytics');\n expect(PROFILE_CARD_SELECT_EVENT_NAME).toBe('vyriy-profile-card.select');\n });\n});\n",
134
+ 'packages/event/constants.ts': "/** Custom event emitted when the profile card is selected. */\nexport const PROFILE_CARD_SELECT_EVENT_NAME = 'vyriy-profile-card.select';\n\n/** Standard OpenMFE analytics event emitted for profile-card interactions. */\nexport const PROFILE_CARD_ANALYTICS_EVENT_NAME = 'openmfe.analytics';\n",
135
+ 'packages/event/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Packages/Event\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
136
+ 'packages/event/index.test.ts': "import { describe, expect, it } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('event public API', () => {\n it('exports profile card event helpers', () => {\n expect(publicApi.PROFILE_CARD_ANALYTICS_EVENT_NAME).toBe('openmfe.analytics');\n expect(publicApi.PROFILE_CARD_SELECT_EVENT_NAME).toBe('vyriy-profile-card.select');\n expect(publicApi.dispatchProfileCardAnalyticsEvent).toBeDefined();\n expect(publicApi.dispatchProfileCardSelectEvent).toBeDefined();\n });\n});\n",
137
+ 'packages/event/index.ts': "export { PROFILE_CARD_ANALYTICS_EVENT_NAME, PROFILE_CARD_SELECT_EVENT_NAME } from './constants.js';\nexport { dispatchProfileCardAnalyticsEvent } from './profile-card-analytics.js';\nexport { dispatchProfileCardSelectEvent } from './profile-card-select.js';\nexport type { ProfileCardAnalyticsData, ProfileCardSelectDetail } from './types.js';\n",
138
+ 'packages/event/profile-card-analytics.test.ts': "import { describe, expect, it, jest } from '@jest/globals';\n\nimport { PROFILE_CARD_ANALYTICS_EVENT_NAME } from './constants.js';\nimport { dispatchProfileCardAnalyticsEvent } from './profile-card-analytics.js';\n\ndescribe('dispatchProfileCardAnalyticsEvent', () => {\n it('dispatches the standard OpenMFE analytics event from the custom element', () => {\n const target = document.createElement('vyriy-profile-card');\n const listener = jest.fn();\n\n target.id = 'profile-card-1';\n target.addEventListener(PROFILE_CARD_ANALYTICS_EVENT_NAME, listener);\n\n const event = dispatchProfileCardAnalyticsEvent(target, {\n avatarUrl: null,\n name: 'Ada Lovelace',\n role: 'Mathematician',\n });\n\n expect(event.type).toBe(PROFILE_CARD_ANALYTICS_EVENT_NAME);\n expect(event.bubbles).toBe(true);\n expect(event.composed).toBe(true);\n expect(event.detail).toEqual({\n action: 'select',\n category: 'profile-card',\n data: {\n avatarUrl: null,\n name: 'Ada Lovelace',\n role: 'Mathematician',\n },\n id: 'profile-card-1',\n name: 'profile_card_select',\n origin: 'vyriy-profile-card',\n variant: null,\n });\n expect(listener).toHaveBeenCalledWith(event);\n });\n});\n",
139
+ 'packages/event/profile-card-analytics.ts': "import { dispatchAnalyticsEvent } from '@vyriy/event';\n\nimport type { ProfileCardAnalyticsData } from './types.js';\n\n/** Dispatches the standard OpenMFE analytics event for a profile-card selection. */\nexport const dispatchProfileCardAnalyticsEvent = (target: HTMLElement, data: ProfileCardAnalyticsData): CustomEvent => {\n return dispatchAnalyticsEvent(\n target,\n {\n action: 'select',\n category: 'profile-card',\n data,\n name: 'profile_card_select',\n },\n {\n bubbles: true,\n composed: true,\n },\n );\n};\n",
140
+ 'packages/event/profile-card-select.test.ts': "import { describe, expect, it, jest } from '@jest/globals';\n\nimport { PROFILE_CARD_SELECT_EVENT_NAME } from './constants.js';\nimport { dispatchProfileCardSelectEvent } from './profile-card-select.js';\n\ndescribe('dispatchProfileCardSelectEvent', () => {\n it('dispatches the profile card select event from the custom element', () => {\n const target = document.createElement('vyriy-profile-card');\n const listener = jest.fn();\n\n target.addEventListener(PROFILE_CARD_SELECT_EVENT_NAME, listener);\n\n const event = dispatchProfileCardSelectEvent(target, {\n avatarUrl: 'https://example.com/avatar.png',\n name: 'Ada Lovelace',\n role: 'Mathematician',\n });\n\n expect(event.type).toBe(PROFILE_CARD_SELECT_EVENT_NAME);\n expect(event.bubbles).toBe(true);\n expect(event.composed).toBe(true);\n expect(event.detail).toEqual({\n avatarUrl: 'https://example.com/avatar.png',\n name: 'Ada Lovelace',\n role: 'Mathematician',\n });\n expect(listener).toHaveBeenCalledWith(event);\n });\n});\n",
141
+ 'packages/event/profile-card-select.ts': "import { dispatchCustomEvent } from '@vyriy/event';\n\nimport { PROFILE_CARD_SELECT_EVENT_NAME } from './constants.js';\nimport type { ProfileCardSelectDetail } from './types.js';\n\n/** Dispatches the profile-card select custom event from the web component host. */\nexport const dispatchProfileCardSelectEvent = (\n target: HTMLElement,\n detail: ProfileCardSelectDetail,\n): CustomEvent<ProfileCardSelectDetail> => {\n return dispatchCustomEvent(target, PROFILE_CARD_SELECT_EVENT_NAME, detail, {\n bubbles: true,\n composed: true,\n });\n};\n",
142
+ 'packages/event/README.md': "# @p/event\n\nTyped profile-card event helpers.\n\n## Usage\n\n```ts\nimport { dispatchProfileCardAnalyticsEvent, dispatchProfileCardSelectEvent } from '@p/event';\n\nconst target = document.querySelector('vyriy-profile-card');\n\nif (target instanceof HTMLElement) {\n const detail = {\n name: 'Ada Lovelace',\n role: 'Mathematician',\n avatarUrl: '/avatar.svg',\n };\n\n dispatchProfileCardSelectEvent(target, detail);\n dispatchProfileCardAnalyticsEvent(target, detail);\n}\n```\n\n## Events\n\n- `vyriy-profile-card.select` is emitted when the profile card is selected.\n- `openmfe.analytics` is emitted with the standard OpenMFE analytics payload for the same selection.\n",
143
+ 'packages/event/types.ts': '/** Detail payload for the profile-card select custom event. */\nexport type ProfileCardSelectDetail = {\n name: string;\n role?: string;\n avatarUrl?: string | null;\n};\n\n/** Analytics data payload emitted for profile-card selections. */\nexport type ProfileCardAnalyticsData = ProfileCardSelectDetail;\n',
144
+ 'packages/query/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Packages/Query\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
145
+ 'packages/query/index.test.ts': "import { describe, expect, it } from '@jest/globals';\n\nimport * as publicApi from './index.js';\n\ndescribe('query public API', () => {\n it('exports query parsers', () => {\n expect(publicApi.parseProfileCardQuery).toBeDefined();\n });\n});\n",
146
+ 'packages/query/index.ts': "export * from './profile-card.js';\nexport type * from './types.js';\n",
147
+ 'packages/query/package.json': '{\n "name": "@p/query",\n "type": "module",\n "private": true\n}\n',
148
+ 'packages/query/profile-card.test.ts': "import { describe, expect, it } from '@jest/globals';\n\nimport { parseProfileCardQuery } from './profile-card.js';\n\ndescribe('parseProfileCardQuery', () => {\n it('returns undefined when name is missing', () => {\n expect(parseProfileCardQuery(undefined)).toBeUndefined();\n expect(parseProfileCardQuery({})).toBeUndefined();\n expect(parseProfileCardQuery({ name: '' })).toBeUndefined();\n });\n\n it('parses supported profile card fields', () => {\n expect(\n parseProfileCardQuery({\n avatarUrl: 'https://example.com/avatar.png',\n description: 'Profile description',\n name: 'Name',\n tags: 'one, two,, three ',\n title: 'Title',\n }),\n ).toEqual({\n avatarUrl: 'https://example.com/avatar.png',\n description: 'Profile description',\n name: 'Name',\n tags: ['one', 'two', 'three'],\n title: 'Title',\n });\n });\n\n it('omits tags when the query list is empty after trimming', () => {\n expect(\n parseProfileCardQuery({\n name: 'Name',\n tags: ', , ',\n }),\n ).toEqual({\n name: 'Name',\n tags: undefined,\n });\n });\n});\n",
149
+ 'packages/query/profile-card.ts': "import type { ParseProfileCardQuery, Query } from './types.js';\n\nconst getQueryValue = (query: Query, name: string): string | undefined => {\n const value = query?.[name];\n\n return value === '' ? undefined : value;\n};\n\nconst getQueryList = (query: Query, name: string): string[] | undefined => {\n const value = getQueryValue(query, name);\n\n if (!value) {\n return undefined;\n }\n\n const list = value\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean);\n\n return list.length > 0 ? list : undefined;\n};\n\n/** Parses request query parameters into profile-card props. */\nexport const parseProfileCardQuery: ParseProfileCardQuery = (query) => {\n const name = getQueryValue(query, 'name');\n\n if (!name) {\n return undefined;\n }\n\n return {\n name,\n title: getQueryValue(query, 'title'),\n description: getQueryValue(query, 'description'),\n avatarUrl: getQueryValue(query, 'avatarUrl'),\n tags: getQueryList(query, 'tags'),\n };\n};\n",
150
+ 'packages/query/README.md': "# @p/query\n\nQuery-string parsers for profile-card API endpoints.\n\n## Exports\n\n- `parseProfileCardQuery(query)` converts router query parameters into profile-card props.\n\n## Example\n\n```ts\nimport { parseProfileCardQuery } from '@p/query';\n\nconst props = parseProfileCardQuery({\n name: 'Ada Lovelace',\n title: 'Mathematician',\n tags: 'math, computing',\n});\n```\n",
151
+ 'packages/query/types.ts': "import type { ProfileCard } from '@p/components/profile-card/types.js';\n\n/** Query string values provided by the API router. */\nexport type Query = Record<string, string | undefined> | undefined;\n\n/** Converts query string values into profile-card props or rejects incomplete input. */\nexport type ParseProfileCardQuery = (query: Query) => ProfileCard | undefined;\n",
152
+ 'stylelint.config.ts': "export { default } from '@vyriy/stylelint-config';\n",
153
+ 'workspaces/api/bin/build.sh': '#!/usr/bin/env sh\n\nset -e\n\nscriptdir="$PWD/workspaces/api";\n\n. "$PWD/workspaces/env.sh"\n\nNODE_ENV=production \\\nnpx webpack --config $scriptdir/webpack.config.ts\n\ncp $scriptdir/package.json dist/api/package.json\nnpm pkg delete "type" --prefix dist/api\nnpm pkg delete "private" --prefix dist/api\n',
154
+ 'workspaces/api/bin/start.sh': '#!/usr/bin/env sh\n\nset -e\n\nscriptdir="$PWD/workspaces/api";\n\n. "$PWD/workspaces/env.sh"\n\necho "Demo:\\n$API/\\n"\necho "Prerender:\\n$API/prerender?name=Developer&title=Senior%20IT%20Professional&avatarUrl=http://localhost:3001/avatar.svg\\n"\necho "Semantic:\\n$API/semantic?name=Developer&title=Senior%20IT%20Professional&avatarUrl=http://localhost:3001/avatar.svg\\n"\necho "Manifest:\\n$API/manifest.yml\\n"\n\nNODE_ENV=production \\\nLOG_LEVEL=info \\\nPORT=$API_PORT \\\ntsx watch $scriptdir/index.ts\n',
155
+ 'workspaces/api/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Workspaces/API\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
156
+ 'workspaces/api/index.test.ts': "import { describe, expect, it, jest } from '@jest/globals';\nimport type { APIGatewayProxyEvent } from '@vyriy/router';\n\nconst apiMock = jest.fn((handler) => ({\n handler,\n}));\nconst serverMock = jest.fn();\n\njest.mock('@vyriy/handler', () => ({\n api: apiMock,\n}));\n\njest.mock('@vyriy/server', () => ({\n server: serverMock,\n}));\n\ndescribe('workspaces/api/index.ts', () => {\n const getEvent = (\n path: string,\n queryStringParameters: APIGatewayProxyEvent['queryStringParameters'] = null,\n ): APIGatewayProxyEvent =>\n ({\n body: null,\n headers: {},\n httpMethod: 'GET',\n path,\n pathParameters: null,\n queryStringParameters,\n }) as APIGatewayProxyEvent;\n\n it('starts the server with the API router handler', async () => {\n process.env.MODE = 'open';\n process.env.TAG = 'vyriy-profile-card';\n process.env.API = 'http://localhost:3000';\n process.env.CDN = 'http://localhost:3001';\n process.env.UI = 'http://localhost:3002';\n\n await import('./index.js');\n\n expect(apiMock).toHaveBeenCalledTimes(1);\n expect(serverMock).toHaveBeenCalledTimes(1);\n expect(serverMock).toHaveBeenCalledWith(apiMock.mock.results[0]?.value);\n\n const handler = apiMock.mock.calls[0]?.[0] as (event: APIGatewayProxyEvent) => Promise<{\n body: string;\n headers?: Record<string, string>;\n statusCode: number;\n }>;\n\n await expect(handler(getEvent('/'))).resolves.toEqual({\n body: expect.stringContaining('<vyriy-profile-card'),\n headers: {\n 'access-control-allow-origin': '*',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'content-type': 'text/html; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n },\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 200,\n });\n\n await expect(handler(getEvent('/prerender'))).resolves.toEqual({\n body: JSON.stringify({\n message: 'Query parameter \"name\" is required.',\n }),\n headers: undefined,\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 400,\n });\n\n const prerenderResponse = await handler(\n getEvent('/prerender', {\n description: 'Profile description',\n name: 'Name',\n tags: 'one, two',\n title: 'Title',\n }),\n );\n\n expect(prerenderResponse).toEqual({\n body: expect.stringContaining('Name'),\n headers: {\n 'access-control-allow-origin': '*',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'content-type': 'text/html; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n },\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 200,\n });\n expect(prerenderResponse.body).toContain('shadowrootmode=\"open\"');\n expect(prerenderResponse.body).toContain('http://localhost:3002/main.css');\n\n await expect(handler(getEvent('/semantic', { name: 'Name' }))).resolves.toEqual({\n body: JSON.stringify({\n '@context': 'https://schema.org',\n '@type': 'Person',\n name: 'Name',\n }),\n headers: {\n 'access-control-allow-origin': '*',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'content-type': 'application/ld+json; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n },\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 200,\n });\n\n await expect(handler(getEvent('/semantic'))).resolves.toEqual({\n body: JSON.stringify({\n message: 'Query parameter \"name\" is required.',\n }),\n headers: undefined,\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 400,\n });\n\n await expect(handler(getEvent('/manifest.yml'))).resolves.toEqual({\n body: expect.stringContaining(\"name: 'Vyriy Profile Card'\"),\n headers: {\n 'access-control-allow-origin': '*',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'content-type': 'text/yaml; charset=utf-8',\n 'x-content-type-options': 'nosniff',\n },\n isBase64Encoded: undefined,\n multiValueHeaders: undefined,\n statusCode: 200,\n });\n\n await expect(handler(getEvent('/healthcheck'))).resolves.toEqual({\n body: JSON.stringify({\n message: 'Not Found',\n }),\n statusCode: 404,\n });\n });\n});\n",
157
+ 'workspaces/api/index.ts': "import { server } from '@vyriy/server';\nimport { api } from '@vyriy/handler';\nimport { createRouter } from '@vyriy/router';\nimport { minify } from '@vyriy/html';\n\nimport { demo, manifest, prerender, semantic } from '@p/api';\nimport { parseProfileCardQuery } from '@p/query';\n\nserver(\n api(async (event) =>\n createRouter()\n .get('/', () => ({\n body: minify(demo()),\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'access-control-allow-origin': '*',\n 'x-content-type-options': 'nosniff',\n },\n }))\n .get('/prerender', ({ query }) => {\n const profileCard = parseProfileCardQuery(query);\n\n if (!profileCard) {\n return {\n statusCode: 400,\n body: JSON.stringify({ message: 'Query parameter \"name\" is required.' }),\n };\n }\n\n return {\n body: minify(prerender(profileCard)),\n headers: {\n 'content-type': 'text/html; charset=utf-8',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'access-control-allow-origin': '*',\n 'x-content-type-options': 'nosniff',\n },\n };\n })\n .get('/semantic', ({ query }) => {\n const profileCard = parseProfileCardQuery(query);\n\n if (!profileCard) {\n return {\n statusCode: 400,\n body: JSON.stringify({ message: 'Query parameter \"name\" is required.' }),\n };\n }\n\n return {\n body: semantic(profileCard),\n headers: {\n 'content-type': 'application/ld+json; charset=utf-8',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'access-control-allow-origin': '*',\n 'x-content-type-options': 'nosniff',\n },\n };\n })\n .get('/manifest.yml', () => ({\n body: manifest(),\n headers: {\n 'content-type': 'text/yaml; charset=utf-8',\n 'cache-control': 'public, max-age=300, s-maxage=3600, stale-while-revalidate=86400',\n 'access-control-allow-origin': '*',\n 'x-content-type-options': 'nosniff',\n },\n }))\n .route(event),\n ),\n);\n",
158
+ 'workspaces/api/package.json': '{\n "name": "@w/api",\n "type": "module",\n "private": true\n}\n',
159
+ 'workspaces/api/README.md': '# app API\n\nCalm cloud-ready application\n',
160
+ 'workspaces/api/webpack.config.ts': "import { EnvironmentPlugin } from 'webpack';\nimport { path } from '@vyriy/path';\nimport { ssr, external } from '@vyriy/webpack-config';\n\nexport default ssr(\n '@w/api',\n {\n path: path('dist', 'api'),\n filename: 'index.js',\n library: { type: 'commonjs2' },\n },\n (config) => ({\n ...config,\n externals: [external({ allowlist: [/^@p/, /^@w/, /^@vyriy/] })],\n plugins: [\n ...(config.plugins ?? []),\n new EnvironmentPlugin([\n 'API',\n 'CDN',\n 'MODE',\n 'TAG',\n 'UI',\n ]),\n ],\n }),\n);\n",
161
+ 'workspaces/env.sh': '#!/usr/bin/env sh\n\n: "${API_PORT:=3000}"\n: "${CDN_PORT:=3001}"\n: "${UI_PORT:=3002}"\n: "${API:=http://localhost:$API_PORT}"\n: "${CDN:=http://localhost:$CDN_PORT}"\n: "${UI:=http://localhost:$UI_PORT}"\n: "${TAG:=vyriy-profile-card}"\n: "${MODE:=open}"\n\nexport API_PORT\nexport CDN_PORT\nexport UI_PORT\nexport API\nexport CDN\nexport UI\nexport TAG\nexport MODE\n',
162
+ 'workspaces/static/bin/build.sh': '#!/usr/bin/env sh\n\nset -e\n\nscriptdir="$PWD/workspaces/static";\ndistdir="$PWD/dist/ui";\n\n. "$PWD/workspaces/env.sh"\n\ncp -R $scriptdir/public/* $distdir/\n',
163
+ 'workspaces/static/bin/start.sh': '#!/usr/bin/env sh\n\nset -e\n\nscriptdir="$PWD/workspaces/static";\n\n. "$PWD/workspaces/env.sh"\n\nnpx serve --cors -p $CDN_PORT $scriptdir/public\n',
164
+ 'workspaces/static/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Workspaces/Static\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
165
+ 'workspaces/static/package.json': '{\n "name": "@w/static",\n "type": "module",\n "private": true\n}\n',
166
+ 'workspaces/static/public/avatar.svg': '<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800">\r\n<rect width="1200" height="800" fill="#0057B7"/>\r\n<rect width="1200" height="400" y="400" fill="#FFD700"/>\r\n</svg>',
167
+ 'workspaces/static/public/icon.svg': '<svg\n width="256"\n height="256"\n viewBox="0 0 256 256"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n role="img"\n aria-labelledby="title description"\n>\n <title id="title">Vyriy Profile Card icon</title>\n <desc id="description">A calm rounded square icon with a profile card symbol.</desc>\n\n <rect width="256" height="256" rx="56" fill="#111827" />\n <rect x="56" y="48" width="144" height="160" rx="24" fill="#F9FAFB" />\n <circle cx="128" cy="104" r="36" fill="#9CA3AF" />\n <rect x="84" y="156" width="88" height="14" rx="7" fill="#111827" />\n <rect x="96" y="180" width="64" height="10" rx="5" fill="#6B7280" />\n <circle cx="184" cy="184" r="20" fill="#10B981" />\n <path\n d="M175 184L181 190L194 176"\n stroke="white"\n stroke-width="6"\n stroke-linecap="round"\n stroke-linejoin="round"\n />\n</svg>\n',
168
+ 'workspaces/static/public/screenshots/compact.svg': '<svg\n width="1280"\n height="720"\n viewBox="0 0 1280 720"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n role="img"\n aria-labelledby="title description"\n>\n <title id="title">Default profile card screenshot</title>\n <desc id="description">A profile card microfrontend rendered in a desktop host page.</desc>\n\n <rect width="1280" height="720" fill="#F3F4F6" />\n <rect x="80" y="72" width="1120" height="576" rx="32" fill="white" />\n\n <rect x="128" y="120" width="360" height="480" rx="28" fill="#F9FAFB" stroke="#E5E7EB" />\n <circle cx="308" cy="244" r="72" fill="#9CA3AF" />\n <rect x="188" y="368" width="240" height="28" rx="14" fill="#111827" />\n <rect x="224" y="420" width="168" height="18" rx="9" fill="#6B7280" />\n <rect x="232" y="492" width="152" height="44" rx="22" fill="#10B981" />\n\n <rect x="560" y="152" width="420" height="40" rx="20" fill="#111827" />\n <rect x="560" y="232" width="520" height="20" rx="10" fill="#6B7280" />\n <rect x="560" y="272" width="460" height="20" rx="10" fill="#9CA3AF" />\n <rect x="560" y="312" width="360" height="20" rx="10" fill="#D1D5DB" />\n <rect x="560" y="420" width="520" height="96" rx="24" fill="#F9FAFB" stroke="#E5E7EB" />\n</svg>\n',
169
+ 'workspaces/static/public/screenshots/default.svg': '<svg\n width="1280"\n height="720"\n viewBox="0 0 1280 720"\n fill="none"\n xmlns="http://www.w3.org/2000/svg"\n role="img"\n aria-labelledby="title description"\n>\n <title id="title">Default profile card screenshot</title>\n <desc id="description">A profile card microfrontend rendered in a desktop host page.</desc>\n\n <rect width="1280" height="720" fill="#F3F4F6" />\n <rect x="80" y="72" width="1120" height="576" rx="32" fill="white" />\n\n <rect x="128" y="120" width="360" height="480" rx="28" fill="#F9FAFB" stroke="#E5E7EB" />\n <circle cx="308" cy="244" r="72" fill="#9CA3AF" />\n <rect x="188" y="368" width="240" height="28" rx="14" fill="#111827" />\n <rect x="224" y="420" width="168" height="18" rx="9" fill="#6B7280" />\n <rect x="232" y="492" width="152" height="44" rx="22" fill="#10B981" />\n\n <rect x="560" y="152" width="420" height="40" rx="20" fill="#111827" />\n <rect x="560" y="232" width="520" height="20" rx="10" fill="#6B7280" />\n <rect x="560" y="272" width="460" height="20" rx="10" fill="#9CA3AF" />\n <rect x="560" y="312" width="360" height="20" rx="10" fill="#D1D5DB" />\n <rect x="560" y="420" width="520" height="96" rx="24" fill="#F9FAFB" stroke="#E5E7EB" />\n</svg>\n',
170
+ 'workspaces/static/README.md': '# @w/static\n\nStatic asset workspace for the profile-card microfrontend.\n\n## Assets\n\n- `avatar.svg` is the default demo avatar.\n- `icon.svg` is the manifest icon.\n- `screenshots/default.svg` and `screenshots/compact.svg` document visible profile-card states.\n\nThe workspace is served as the CDN origin during local development.\n',
171
+ 'workspaces/ui/bin/build.sh': '#!/usr/bin/env sh\n\nset -e\n\nscriptdir="$PWD/workspaces/ui";\n\n. "$PWD/workspaces/env.sh"\n\nNODE_ENV=production \\\nnpx webpack \\\n--config $scriptdir/webpack.config.ts\n',
172
+ 'workspaces/ui/bin/start.sh': '#!/usr/bin/env sh\n\nset -e\n\nscriptdir="$PWD/workspaces/ui";\n\n. "$PWD/workspaces/env.sh"\n\nnpx webpack serve \\\n--open \\\n--config $scriptdir/webpack.config.ts \\\n--port $UI_PORT\n',
173
+ 'workspaces/ui/doc.mdx': "import { Meta, Markdown } from '@storybook/addon-docs/blocks';\nimport ReadMe from './README.md?raw';\n\n<Meta title=\"Workspaces/UI\" />\n\n<Markdown>{ReadMe}</Markdown>\n",
174
+ 'workspaces/ui/index.test.tsx': "import { beforeAll, describe, expect, it, jest } from '@jest/globals';\nimport type { ReactElement } from 'react';\n\ntype CustomElementOptions = {\n tag: string;\n elements: () => {\n elements: HTMLElement[];\n root: HTMLElement;\n };\n render: (el: HTMLElement) => ReactElement<{\n onClick: () => void;\n tags?: string[];\n }>;\n};\n\nconst customElementMock = jest.fn();\nconst dispatchProfileCardAnalyticsEventMock = jest.fn();\nconst dispatchProfileCardSelectEventMock = jest.fn();\nlet options: CustomElementOptions;\n\njest.mock('@vyriy/render/custom-element', () => ({\n customElement: customElementMock,\n}));\n\njest.mock('@p/env', () => ({\n getUi: () => 'http://localhost:3002',\n}));\n\njest.mock('@p/event', () => ({\n dispatchProfileCardAnalyticsEvent: dispatchProfileCardAnalyticsEventMock,\n dispatchProfileCardSelectEvent: dispatchProfileCardSelectEventMock,\n}));\n\ndescribe('workspaces/ui/index.tsx', () => {\n beforeAll(async () => {\n await import('./index.js');\n\n [options] = (customElementMock.mock.calls[0] ?? []) as [CustomElementOptions];\n });\n\n it('registers the profile-card custom element with event dispatching', () => {\n const renderElements = options.elements();\n const host = document.createElement('vyriy-profile-card');\n\n host.setAttribute('avatarUrl', 'https://example.com/avatar.png');\n host.setAttribute('description', 'Profile description');\n host.setAttribute('name', 'Ada Lovelace');\n host.setAttribute('tags', '[\"math\",\"computing\"]');\n host.setAttribute('title', 'Mathematician');\n\n const element = options.render(host);\n\n element.props.onClick();\n\n expect(options.tag).toBe('vyriy-profile-card');\n expect(renderElements.root).toBeInstanceOf(HTMLDivElement);\n expect(renderElements.elements[0]).toBeInstanceOf(HTMLLinkElement);\n expect((renderElements.elements[0] as HTMLLinkElement).href).toBe('http://localhost:3002/main.css');\n expect(element.props.tags).toEqual(['math', 'computing']);\n expect(dispatchProfileCardSelectEventMock).toHaveBeenCalledWith(host, {\n avatarUrl: 'https://example.com/avatar.png',\n name: 'Ada Lovelace',\n role: 'Mathematician',\n });\n expect(dispatchProfileCardAnalyticsEventMock).toHaveBeenCalledWith(host, {\n avatarUrl: 'https://example.com/avatar.png',\n name: 'Ada Lovelace',\n role: 'Mathematician',\n });\n });\n\n it('ignores missing and invalid tags attributes', () => {\n const host = document.createElement('vyriy-profile-card');\n\n expect(options.render(host).props.tags).toBeUndefined();\n\n host.setAttribute('tags', 'not json');\n\n expect(options.render(host).props.tags).toBeUndefined();\n\n host.setAttribute('tags', '{\"tag\":\"math\"}');\n\n expect(options.render(host).props.tags).toBeUndefined();\n });\n\n it('falls back to empty profile values when attributes are missing', () => {\n const host = document.createElement('vyriy-profile-card');\n const element = options.render(host);\n\n element.props.onClick();\n\n expect(dispatchProfileCardSelectEventMock).toHaveBeenLastCalledWith(host, {\n avatarUrl: null,\n name: '',\n role: undefined,\n });\n });\n});\n",
175
+ 'workspaces/ui/index.tsx': "import { customElement } from '@vyriy/render/custom-element';\n\nimport { getUi } from '@p/env';\nimport { dispatchProfileCardAnalyticsEvent, dispatchProfileCardSelectEvent } from '@p/event';\n\nimport { ProfileCard } from '@p/components/profile-card';\nimport '@p/components/styles.scss';\n\nconst parseJsonAttribute = (value: string | null): string[] | undefined => {\n if (!value) {\n return undefined;\n }\n\n try {\n const parsedValue: unknown = JSON.parse(value);\n\n if (Array.isArray(parsedValue) && parsedValue.every((item) => typeof item === 'string')) {\n return parsedValue;\n }\n } catch {\n return undefined;\n }\n\n return undefined;\n};\n\nconst getProfileCardSelectDetail = (el: HTMLElement) => ({\n avatarUrl: el.getAttribute('avatarUrl'),\n name: el.getAttribute('name') ?? '',\n role: el.getAttribute('title') ?? undefined,\n});\n\ncustomElement({\n tag: 'vyriy-profile-card',\n elements: () => {\n const link = document.createElement('link');\n const container = document.createElement('div');\n\n link.rel = 'stylesheet';\n link.href = `${getUi()}/main.css`;\n\n return {\n elements: [link, container],\n root: container,\n };\n },\n render: (el) => (\n <ProfileCard\n avatarUrl={el.getAttribute('avatarUrl') ?? ''}\n description={el.getAttribute('description') ?? ''}\n name={el.getAttribute('name') ?? ''}\n onClick={() => {\n const detail = getProfileCardSelectDetail(el);\n\n dispatchProfileCardSelectEvent(el, detail);\n dispatchProfileCardAnalyticsEvent(el, detail);\n }}\n tags={parseJsonAttribute(el.getAttribute('tags'))}\n title={el.getAttribute('title') ?? ''}\n />\n ),\n});\n",
176
+ 'workspaces/ui/package.json': '{\n "name": "@w/ui",\n "type": "module",\n "private": true\n}\n',
177
+ 'workspaces/ui/README.md': '# @w/ui\n\nDemo entry point.\n',
178
+ 'workspaces/ui/webpack.config.ts': "import { EnvironmentPlugin } from 'webpack';\n\nimport { csr, html } from '@vyriy/webpack-config';\nimport { path } from '@vyriy/path';\n\nexport default csr(\n '@w/ui',\n {\n path: path('dist', 'cdn'),\n filename: 'index.js',\n },\n (config) => ({\n ...config,\n plugins: [\n ...(config.plugins ?? []),\n new EnvironmentPlugin(['API', 'CDN', 'UI']),\n html(\n {\n htmlAttributes: 'lang=\"en\"',\n title: '<title>Demo</title>',\n meta: '<meta charset=\"utf-8\" /><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />',\n body: '<vyriy-profile-card name=\"Developer\" title=\"Senior IT Professional\" avatarUrl=\"http://localhost:3001/avatar.svg\"></vyriy-profile-card>',\n script: [\n '<script defer=\"defer\" src=\"/index.js\"></script>',\n [\n '<script type=\"module\">',\n \"const card = document.querySelector('vyriy-profile-card');\",\n \"card.addEventListener('vyriy-profile-card.select', (event) => {\",\n \" console.log('UI demo event:', event.detail);\",\n '});',\n '</scr' + 'ipt>',\n ].join(''),\n ].join(''),\n },\n { inject: false },\n ),\n ],\n }),\n);\n",
179
+ };
2
180
  export const mfe = {
3
181
  files: (options) => ({
4
182
  ...base.files(options),
183
+ ...projectFiles,
184
+ 'package.json': JSON.stringify({
185
+ name: options.name,
186
+ version: '0.0.0',
187
+ description: options.description,
188
+ private: true,
189
+ type: 'module',
190
+ agents: './AGENTS.md',
191
+ packageManager: packageJson.packageManager,
192
+ engines: {
193
+ node: packageJson.engines.node,
194
+ },
195
+ workspaces: [
196
+ 'packages/*',
197
+ 'workspaces/*',
198
+ ],
199
+ scripts: {
200
+ storybook: 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook dev -p 6006 --disable-telemetry',
201
+ check: 'run-s lint build test',
202
+ fix: "run-s 'fix:*'",
203
+ start: "run-p 'start:*'",
204
+ lint: "run-s 'lint:*'",
205
+ build: "run-s 'build:*'",
206
+ test: "run-s 'test:*'",
207
+ 'fix:prettier': 'prettier . --write',
208
+ 'fix:eslint': 'eslint . --fix',
209
+ 'fix:stylelint': "stylelint '**/*.{css,scss}' --fix",
210
+ 'start:api': 'sh workspaces/api/bin/start.sh',
211
+ 'start:static': 'sh workspaces/static/bin/start.sh',
212
+ 'start:ui': 'sh workspaces/ui/bin/start.sh',
213
+ 'lint:ts': 'tsc',
214
+ 'lint:prettier': 'prettier . --check',
215
+ 'lint:eslint': 'eslint .',
216
+ 'lint:stylelint': "stylelint '**/*.{css,scss}'",
217
+ 'build:api': 'sh workspaces/api/bin/build.sh',
218
+ 'build:static': 'sh workspaces/api/bin/build.sh',
219
+ 'build:ui': 'sh workspaces/ui/bin/build.sh',
220
+ 'build:storybook': 'cross-env STORYBOOK_DISABLE_TELEMETRY=1 storybook build --quiet --disable-telemetry',
221
+ 'test:jest': 'jest',
222
+ prebuild: 'rimraf dist',
223
+ postinstall: 'husky',
224
+ },
225
+ dependencies: {
226
+ '@testing-library/dom': packageJson.peerDependencies['@testing-library/dom'],
227
+ '@testing-library/react': packageJson.peerDependencies['@testing-library/react'],
228
+ '@types/jest': packageJson.peerDependencies['@types/jest'],
229
+ '@vyriy/browserslist-config': `^${packageJson.version}`,
230
+ '@vyriy/cn': `^${packageJson.version}`,
231
+ '@vyriy/env': `^${packageJson.version}`,
232
+ '@vyriy/eslint-config': `^${packageJson.version}`,
233
+ '@vyriy/event': `^${packageJson.version}`,
234
+ '@vyriy/handler': `^${packageJson.version}`,
235
+ '@vyriy/html': `^${packageJson.version}`,
236
+ '@vyriy/jest-config': `^${packageJson.version}`,
237
+ '@vyriy/path': `^${packageJson.version}`,
238
+ '@vyriy/prettier-config': `^${packageJson.version}`,
239
+ '@vyriy/render': `^${packageJson.version}`,
240
+ '@vyriy/router': `^${packageJson.version}`,
241
+ '@vyriy/server': `^${packageJson.version}`,
242
+ '@vyriy/storybook-config': `^${packageJson.version}`,
243
+ '@vyriy/stylelint-config': `^${packageJson.version}`,
244
+ '@vyriy/typescript-config': `^${packageJson.version}`,
245
+ '@vyriy/webpack-config': `^${packageJson.version}`,
246
+ 'cross-env': packageJson.peerDependencies['cross-env'],
247
+ eslint: packageJson.peerDependencies['eslint'],
248
+ husky: packageJson.peerDependencies['husky'],
249
+ jest: packageJson.peerDependencies['jest'],
250
+ 'npm-run-all2': packageJson.peerDependencies['npm-run-all2'],
251
+ prettier: packageJson.peerDependencies['prettier'],
252
+ rimraf: packageJson.peerDependencies['rimraf'],
253
+ serve: packageJson.peerDependencies['serve'],
254
+ storybook: packageJson.peerDependencies['storybook'],
255
+ stylelint: packageJson.peerDependencies['stylelint'],
256
+ tsx: packageJson.peerDependencies['tsx'],
257
+ typescript: packageJson.peerDependencies['typescript'],
258
+ webpack: packageJson.peerDependencies['webpack'],
259
+ 'webpack-cli': packageJson.peerDependencies['webpack-cli'],
260
+ },
261
+ }, null, 2) + '\n',
262
+ 'README.md': `# ${options.name}
263
+
264
+ ${options.description}
265
+
266
+ ## Setup
267
+
268
+ \`\`\`bash
269
+ yarn install
270
+ \`\`\`
271
+
272
+ ## Start
273
+
274
+ Start the API, static asset server, and UI dev server together:
275
+
276
+ \`\`\`bash
277
+ yarn start
278
+ \`\`\`
279
+
280
+ Start individual workspaces:
281
+
282
+ \`\`\`bash
283
+ yarn start:api
284
+ yarn start:static
285
+ yarn start:ui
286
+ \`\`\`
287
+
288
+ ## Local URLs
289
+
290
+ Default ports are defined in \`workspaces/env.sh\`:
291
+
292
+ - API: \`http://localhost:3000\`
293
+ - Static/CDN assets: \`http://localhost:3001\`
294
+ - UI dev server: \`http://localhost:3002\`
295
+
296
+ Useful API URLs:
297
+
298
+ - Demo HTML: \`http://localhost:3000/\`
299
+ - Prerendered custom element: \`http://localhost:3000/prerender?name=Developer&title=Senior%20IT%20Professional&avatarUrl=http://localhost:3001/avatar.svg\`
300
+ - Semantic JSON-LD: \`http://localhost:3000/semantic?name=Developer&title=Senior%20IT%20Professional&avatarUrl=http://localhost:3001/avatar.svg\`
301
+ - Manifest: \`http://localhost:3000/manifest.yml\`
302
+
303
+ ## Validation
304
+
305
+ \`\`\`bash
306
+ yarn lint
307
+ yarn test
308
+ yarn build
309
+ \`\`\`
310
+ `,
311
+ 'doc.mdx': `import { Meta, Markdown } from '@storybook/addon-docs/blocks';
312
+ import ReadMe from './README.md?raw';
313
+
314
+ <Meta title="${options.name}" />
315
+
316
+ <Markdown>{ReadMe}</Markdown>
317
+ `,
318
+ 'packages/components/package.json': JSON.stringify({
319
+ name: '@p/components',
320
+ type: 'module',
321
+ private: true,
322
+ }, null, 2) + '\n',
323
+ 'packages/event/package.json': JSON.stringify({
324
+ name: '@p/event',
325
+ type: 'module',
326
+ private: true,
327
+ }, null, 2) + '\n',
5
328
  }),
6
329
  ci: {
7
- github: {
8
- '.gitlab-ci.yml': 'code',
9
- },
10
- },
11
- deploy: {
12
- docker: {
13
- Dockerfile: 'code',
14
- },
330
+ ...base.ci,
15
331
  },
332
+ deploy: {},
16
333
  };