kmcom-nuxt-layers 2.2.12 → 2.3.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.
Files changed (102) hide show
  1. package/docs/FEEDS.md +1 -2
  2. package/layers/animations/app/composables/useMagneticElement.ts +11 -9
  3. package/layers/animations/app/composables/useTiltEffect.ts +11 -9
  4. package/layers/animations/app/utils/pointerMotion.ts +31 -0
  5. package/layers/canvas/app/components/ShaderCanvas.vue +2 -2
  6. package/layers/content/app/composables/useCollectionItems.ts +28 -0
  7. package/layers/content/app/composables/useGalleryItems.ts +8 -14
  8. package/layers/content/app/composables/usePortfolioItems.ts +10 -18
  9. package/layers/core/app/composables/useBrowser.ts +9 -82
  10. package/layers/core/app/composables/useFeatures.ts +3 -27
  11. package/layers/core/app/plugins/init.ts +157 -135
  12. package/layers/core/app/utils/browserInfo.ts +115 -0
  13. package/layers/core/app/utils/featureClasses.ts +40 -0
  14. package/layers/core/app/utils/helpers.test.ts +51 -0
  15. package/layers/feeds/app/components/Feeds/Index.vue +1 -1
  16. package/layers/feeds/app/components/Feeds/RouteCard.vue +3 -9
  17. package/layers/feeds/app/utils/feed-catalog.ts +9 -4
  18. package/layers/feeds/nuxt.config.ts +0 -1
  19. package/layers/feeds/server/utils/content-adapter.test.ts +68 -0
  20. package/layers/feeds/server/utils/content-adapter.ts +2 -22
  21. package/layers/feeds/server/utils/feed-author.ts +32 -0
  22. package/layers/feeds/server/utils/feed-config.ts +88 -0
  23. package/layers/feeds/server/utils/feed-service.ts +11 -30
  24. package/layers/feeds/server/utils/feed-xml.ts +26 -0
  25. package/layers/feeds/server/utils/formats/rss.ts +10 -15
  26. package/layers/feeds/server/utils/formats.test.ts +71 -0
  27. package/layers/forms/app/components/Form/Field.vue +42 -30
  28. package/layers/forms/app/utils/fieldProps.ts +65 -0
  29. package/layers/layout/app/components/Layout/Grid/Item.vue +29 -146
  30. package/layers/layout/app/utils/gridPlacementStyle.ts +195 -0
  31. package/layers/mailer/app/types/mailer.ts +7 -25
  32. package/layers/mailer/server/utils/email.ts +28 -13
  33. package/layers/mailer/server/utils/hooks.ts +1 -20
  34. package/layers/navigation/app/composables/useSite.ts +2 -9
  35. package/layers/navigation/app/utils/site.ts +26 -0
  36. package/layers/routing/app/utils/resolveRoute.test.ts +47 -0
  37. package/layers/routing/app/utils/resolveRoute.ts +19 -10
  38. package/layers/scripts/app/composables/useAnalytics.ts +8 -41
  39. package/layers/scripts/app/composables/useGtm.ts +6 -13
  40. package/layers/scripts/app/utils/scriptClients.ts +70 -0
  41. package/layers/scroll/app/composables/useSmoothScroll.ts +9 -43
  42. package/layers/scroll/app/utils/scroll.ts +103 -0
  43. package/layers/seo/app/composables/useSeoConfig.ts +3 -9
  44. package/layers/seo/app/utils/seoConfig.ts +38 -0
  45. package/layers/shader/app/components/Material/AmbientAurora.client.vue +11 -33
  46. package/layers/shader/app/components/Material/AmbientFlow.client.vue +10 -37
  47. package/layers/shader/app/components/Material/AmbientGradientMesh.client.vue +10 -37
  48. package/layers/shader/app/components/Material/AmbientNebula.client.vue +12 -37
  49. package/layers/shader/app/components/Material/AmbientOcean.client.vue +9 -33
  50. package/layers/shader/app/components/Material/Gradient.client.vue +25 -46
  51. package/layers/shader/app/components/Material/Image.client.vue +10 -55
  52. package/layers/shader/app/components/Material/Node.client.vue +18 -5
  53. package/layers/shader/app/components/Material/Noise.client.vue +9 -43
  54. package/layers/shader/app/components/Preset/ThemeBubble.client.vue +2 -1
  55. package/layers/shader/app/components/Preset/ThemeFlow.client.vue +2 -1
  56. package/layers/shader/app/components/Preset/ThemeGradient.client.vue +2 -1
  57. package/layers/shader/app/components/Preset/ThemeLavaLamp.client.vue +2 -1
  58. package/layers/shader/app/components/Preset/ThemePlasma.client.vue +2 -1
  59. package/layers/shader/app/components/Preset/ThemeWave.client.vue +2 -1
  60. package/layers/shader/app/components/Shader/Background.client.vue +44 -24
  61. package/layers/shader/app/composables/useAmbientMaterials.ts +5 -1
  62. package/layers/shader/app/composables/useShader.ts +38 -23
  63. package/layers/shader/app/composables/useShaderGraph.ts +11 -6
  64. package/layers/shader/app/composables/useShaderMixBlend.ts +4 -4
  65. package/layers/shader/app/composables/useShaderRuntime.ts +0 -1
  66. package/layers/shader/app/composables/useShaderVec2.ts +2 -4
  67. package/layers/shader/app/composables/useThemePreset.ts +34 -8
  68. package/layers/shader/app/composables/useUniformWatchers.ts +15 -0
  69. package/layers/shader/app/composables/useUniforms.ts +0 -1
  70. package/layers/shader/app/shaders/common/blend.ts +4 -4
  71. package/layers/shader/app/shaders/common/effects.ts +38 -21
  72. package/layers/shader/app/shaders/common/grain.ts +46 -49
  73. package/layers/shader/app/shaders/common/lighting.ts +17 -15
  74. package/layers/shader/app/shaders/common/math.ts +2 -4
  75. package/layers/shader/app/shaders/common/nodes.ts +17 -0
  76. package/layers/shader/app/shaders/common/palette.ts +21 -11
  77. package/layers/shader/app/shaders/common/patterns.ts +25 -14
  78. package/layers/shader/app/shaders/common/shapes.ts +97 -88
  79. package/layers/shader/app/shaders/common/uv.ts +33 -34
  80. package/layers/shader/app/shaders/createMaterial.ts +92 -78
  81. package/layers/shader/app/shaders/layers/paperShading.ts +22 -10
  82. package/layers/shader/app/shaders/layers/shaderGradient.ts +46 -21
  83. package/layers/shader/app/utils/tsl/tween.ts +2 -4
  84. package/layers/shader/package.json +5 -1
  85. package/layers/starter/app/components/StarterDesignSystem.vue +1913 -0
  86. package/layers/starter/app/components/StarterHome.vue +407 -0
  87. package/layers/starter/nuxt.config.ts +15 -0
  88. package/layers/starter/package.json +10 -0
  89. package/layers/theme/app/components/ThemePicker/Menu.vue +3 -25
  90. package/layers/theme/app/composables/useThemePreferenceModels.ts +39 -0
  91. package/layers/theme/server/plugins/theme-fouc.ts +1 -92
  92. package/layers/theme/server/utils/accent-css.ts +75 -0
  93. package/layers/typography/app/composables/typography.ts +3 -7
  94. package/layers/visual/app/composables/accent.ts +2 -9
  95. package/layers/visual/app/composables/gradient.ts +33 -46
  96. package/layers/visual/app/composables/picture.ts +2 -79
  97. package/layers/visual/app/utils/colorTokens.ts +23 -0
  98. package/layers/visual/app/utils/gradientStyle.ts +41 -0
  99. package/layers/visual/app/utils/responsiveSizes.ts +49 -0
  100. package/package.json +17 -5
  101. package/layers/feeds/app/utils/feed-catalog.test.ts +0 -71
  102. package/layers/feeds/server/routes/feed/discovery.get.ts +0 -31
@@ -2,6 +2,7 @@ import { queryCollection } from '@nuxt/content/nitro'
2
2
  import type { H3Event } from 'h3'
3
3
 
4
4
  import type { FeedItem } from './types'
5
+ import { resolveFeedAuthor, resolveFeedDate } from './feed-author'
5
6
 
6
7
  type FeedSourceAuthor = {
7
8
  name?: string | undefined
@@ -22,7 +23,7 @@ type FeedSourceItem = {
22
23
  }
23
24
 
24
25
  type FeedCollectionQuery = {
25
- all(): Promise<FeedSourceItem[]>
26
+ all: () => Promise<FeedSourceItem[]>
26
27
  }
27
28
 
28
29
  const getFeedCollection = queryCollection as unknown as (
@@ -30,27 +31,6 @@ const getFeedCollection = queryCollection as unknown as (
30
31
  collection: string
31
32
  ) => FeedCollectionQuery
32
33
 
33
- function resolveFeedAuthor(item: FeedSourceItem): string | undefined {
34
- const firstAuthor = item.authors?.[0]
35
- if (typeof firstAuthor?.name === 'string' && firstAuthor.name.length > 0) {
36
- return firstAuthor.name
37
- }
38
-
39
- if (typeof item.author === 'string' && item.author.length > 0) {
40
- return item.author
41
- }
42
-
43
- if (item.author && typeof item.author === 'object' && typeof item.author.name === 'string') {
44
- return item.author.name
45
- }
46
-
47
- return undefined
48
- }
49
-
50
- function resolveFeedDate(item: FeedSourceItem): Date {
51
- return new Date(item.date ?? item.createdAt ?? Date.now())
52
- }
53
-
54
34
  export async function getContentFeedItems(
55
35
  event: H3Event,
56
36
  collection: string = 'blog',
@@ -0,0 +1,32 @@
1
+ type FeedSourceAuthor = {
2
+ name?: string | undefined
3
+ }
4
+
5
+ type FeedSourceItem = {
6
+ author?: FeedSourceAuthor | string | undefined
7
+ authors?: FeedSourceAuthor[] | undefined
8
+ }
9
+
10
+ export function resolveFeedAuthor(item: FeedSourceItem): string | undefined {
11
+ const firstAuthor = item.authors?.[0]
12
+ if (typeof firstAuthor?.name === 'string' && firstAuthor.name.length > 0) {
13
+ return firstAuthor.name
14
+ }
15
+
16
+ if (typeof item.author === 'string' && item.author.length > 0) {
17
+ return item.author
18
+ }
19
+
20
+ if (item.author && typeof item.author === 'object' && typeof item.author.name === 'string') {
21
+ return item.author.name
22
+ }
23
+
24
+ return undefined
25
+ }
26
+
27
+ export function resolveFeedDate(item: {
28
+ date?: string | number | Date | undefined
29
+ createdAt?: string | number | Date | undefined
30
+ }): Date {
31
+ return new Date(item.date ?? item.createdAt ?? Date.now())
32
+ }
@@ -0,0 +1,88 @@
1
+ import type { SiteConfig } from '#layers/core/app/types/site'
2
+
3
+ import type { FeedConfig } from './types'
4
+
5
+ type AppFeedConfig = {
6
+ site?: SiteConfig
7
+ feedsLayer?: {
8
+ feed?: {
9
+ limit?: number
10
+ defaultCollection?: string
11
+ }
12
+ }
13
+ }
14
+
15
+ type FeedLayerConfig = NonNullable<AppFeedConfig['feedsLayer']>['feed']
16
+
17
+ function resolveFeedTitle(site: SiteConfig, collection: string | undefined) {
18
+ const collectionLabel = collection
19
+ ? ` — ${collection.charAt(0).toUpperCase() + collection.slice(1)}`
20
+ : ''
21
+
22
+ return `${site.title ?? 'My Site'}${collectionLabel}`
23
+ }
24
+
25
+ function resolveFeedAuthorName(author: SiteConfig['author']) {
26
+ const authorName = author?.name ?? ''
27
+ return authorName.length > 0 ? authorName : undefined
28
+ }
29
+
30
+ function resolveFeedAuthor(site: SiteConfig) {
31
+ const authorName = resolveFeedAuthorName(site.author)
32
+ return authorName
33
+ ? { name: authorName, email: site.author?.email, link: site.author?.link }
34
+ : undefined
35
+ }
36
+
37
+ function resolveFeedCopyright(site: SiteConfig) {
38
+ const authorName = resolveFeedAuthorName(site.author)
39
+ if (site.copyright) return site.copyright
40
+ if (!authorName) return undefined
41
+ return `Copyright ${new Date().getFullYear()} ${authorName}`
42
+ }
43
+
44
+ function resolveConfiguredFeedLimit(feedConfig: FeedLayerConfig) {
45
+ return feedConfig?.limit ?? 30
46
+ }
47
+
48
+ export function resolveFeedSiteConfig(appConfig: AppFeedConfig, requestUrl: URL) {
49
+ const site = appConfig.site ?? {}
50
+ const origin = `${requestUrl.protocol}//${requestUrl.host}`
51
+ return {
52
+ site,
53
+ siteUrl: (site.url as string | undefined)?.replace(/\/$/, '') || origin,
54
+ }
55
+ }
56
+
57
+ export function resolveFeedCollection(
58
+ feedConfig: FeedLayerConfig,
59
+ collection?: string
60
+ ) {
61
+ return collection ?? feedConfig?.defaultCollection ?? 'blog'
62
+ }
63
+
64
+ export function createFeedConfig(args: {
65
+ site: SiteConfig
66
+ siteUrl: string
67
+ collection: string | undefined
68
+ }): FeedConfig {
69
+ const { site, siteUrl, collection } = args
70
+
71
+ return {
72
+ title: resolveFeedTitle(site, collection),
73
+ description: site.description ?? '',
74
+ siteUrl,
75
+ author: resolveFeedAuthor(site),
76
+ image: site.image || undefined,
77
+ favicon: site.favicon ?? '/favicon.ico',
78
+ copyright: resolveFeedCopyright(site),
79
+ }
80
+ }
81
+
82
+ export function resolveFeedLimit(
83
+ feedConfig: FeedLayerConfig,
84
+ options?: { unlimited?: boolean | undefined }
85
+ ) {
86
+ if (options?.unlimited) return Infinity
87
+ return resolveConfiguredFeedLimit(feedConfig)
88
+ }
@@ -1,7 +1,12 @@
1
- import type { SiteConfig } from '#layers/core/app/types/site'
2
1
  import type { H3Event } from 'h3'
3
2
 
4
3
  import type { FeedConfig, FeedItem } from './types'
4
+ import {
5
+ createFeedConfig,
6
+ resolveFeedCollection,
7
+ resolveFeedLimit,
8
+ resolveFeedSiteConfig,
9
+ } from './feed-config'
5
10
 
6
11
  export async function buildFeed(
7
12
  event: H3Event,
@@ -9,37 +14,13 @@ export async function buildFeed(
9
14
  options?: { unlimited?: boolean }
10
15
  ): Promise<{ items: FeedItem[]; config: FeedConfig }> {
11
16
  const appConfig = useAppConfig()
12
- const site: SiteConfig = (appConfig as { site?: SiteConfig }).site ?? {}
13
- const feedConfig =
14
- (appConfig as { feedsLayer?: { feed?: { limit?: number; defaultCollection?: string } } })
15
- .feedsLayer?.feed ?? {}
16
- const limit: number = options?.unlimited ? Infinity : (feedConfig.limit ?? 30)
17
- const defaultCollection = feedConfig.defaultCollection ?? 'blog'
18
-
17
+ const feedConfig = appConfig.feedsLayer?.feed
19
18
  const requestUrl = getRequestURL(event)
20
- const origin = `${requestUrl.protocol}//${requestUrl.host}`
21
- const siteUrl = (site.url as string | undefined)?.replace(/\/$/, '') || origin
22
-
23
- const authorName = site.author?.name ?? ''
24
- const resolvedCollection = collection ?? defaultCollection
25
- const collectionLabel = collection
26
- ? ` — ${collection.charAt(0).toUpperCase() + collection.slice(1)}`
27
- : ''
28
-
29
- const config: FeedConfig = {
30
- title: `${site.title ?? 'My Site'}${collectionLabel}`,
31
- description: site.description ?? '',
32
- siteUrl,
33
- author: authorName
34
- ? { name: authorName, email: site.author?.email, link: site.author?.link }
35
- : undefined,
36
- image: site.image || undefined,
37
- favicon: site.favicon ?? '/favicon.ico',
38
- copyright:
39
- site.copyright ||
40
- (authorName ? `Copyright ${new Date().getFullYear()} ${authorName}` : undefined),
41
- }
19
+ const { site, siteUrl } = resolveFeedSiteConfig(appConfig, requestUrl)
20
+ const resolvedCollection = resolveFeedCollection(feedConfig, collection)
21
+ const limit = resolveFeedLimit(feedConfig, options)
42
22
 
23
+ const config = createFeedConfig({ site, siteUrl, collection })
43
24
  const items = await getContentFeedItems(event, resolvedCollection, limit)
44
25
 
45
26
  return { items, config }
@@ -0,0 +1,26 @@
1
+ import type { FeedConfig, FeedItem } from './types'
2
+
3
+ export function resolveFeedItemId(siteUrl: string, item: FeedItem) {
4
+ return `${siteUrl}${item.id}`
5
+ }
6
+
7
+ export function resolveFeedItemLink(siteUrl: string, item: FeedItem) {
8
+ return `${siteUrl}${item.link}`
9
+ }
10
+
11
+ export function applyFeedStylesheet(xml: string) {
12
+ return xml.replace(
13
+ /<\?xml version="1\.0" encoding="utf-8"\?>/i,
14
+ '<?xml version="1.0" encoding="UTF-8"?>\n<?xml-stylesheet type="text/xsl" href="/feed/style.xsl"?>'
15
+ )
16
+ }
17
+
18
+ export function resolveFeedAuthorPayload(config: FeedConfig['author']) {
19
+ return config
20
+ ? {
21
+ name: config.name,
22
+ ...(config.email ? { email: config.email } : {}),
23
+ ...(config.link ? { link: config.link } : {}),
24
+ }
25
+ : undefined
26
+ }
@@ -1,16 +1,15 @@
1
1
  import { Feed } from 'feed'
2
2
 
3
3
  import type { FeedConfig, FeedItem } from '../types'
4
+ import {
5
+ applyFeedStylesheet,
6
+ resolveFeedAuthorPayload,
7
+ resolveFeedItemId,
8
+ resolveFeedItemLink,
9
+ } from '../feed-xml'
4
10
 
5
11
  export function toRSS(items: FeedItem[], config: FeedConfig): string {
6
- const author = config.author
7
- ? {
8
- name: config.author.name,
9
- ...(config.author.email ? { email: config.author.email } : {}),
10
- ...(config.author.link ? { link: config.author.link } : {}),
11
- }
12
- : undefined
13
-
12
+ const author = resolveFeedAuthorPayload(config.author)
14
13
  const feed = new Feed({
15
14
  title: config.title,
16
15
  description: config.description,
@@ -26,17 +25,13 @@ export function toRSS(items: FeedItem[], config: FeedConfig): string {
26
25
  for (const item of items) {
27
26
  feed.addItem({
28
27
  title: item.title,
29
- id: `${config.siteUrl}${item.id}`,
30
- link: `${config.siteUrl}${item.link}`,
28
+ id: resolveFeedItemId(config.siteUrl, item),
29
+ link: resolveFeedItemLink(config.siteUrl, item),
31
30
  ...(item.description ? { description: item.description } : {}),
32
31
  date: item.date,
33
32
  ...(item.author ? { author: [{ name: item.author }] } : {}),
34
33
  })
35
34
  }
36
35
 
37
- const raw = feed.rss2()
38
- return raw.replace(
39
- '<?xml version="1.0" encoding="UTF-8"?>',
40
- '<?xml version="1.0" encoding="UTF-8"?>\n<?xml-stylesheet type="text/xsl" href="/feed/style.xsl"?>'
41
- )
36
+ return applyFeedStylesheet(feed.rss2())
42
37
  }
@@ -0,0 +1,71 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import type { FeedConfig, FeedItem } from './types'
4
+ import { toAtom } from './formats/atom'
5
+ import { toJSONFeed } from './formats/json'
6
+ import { toRSS } from './formats/rss'
7
+
8
+ const config: FeedConfig = {
9
+ title: 'Nuxt Layers Feed',
10
+ description: 'Layered syndication output.',
11
+ siteUrl: 'https://example.com',
12
+ author: {
13
+ name: 'Kieran Mansfield',
14
+ link: 'https://example.com/about',
15
+ },
16
+ favicon: '/favicon.ico',
17
+ }
18
+
19
+ const items: FeedItem[] = [
20
+ {
21
+ title: 'A & B',
22
+ description: 'Read > write',
23
+ link: '/posts/a-b',
24
+ id: '/posts/a-b',
25
+ date: new Date('2026-01-03T10:00:00.000Z'),
26
+ author: 'Ada Lovelace',
27
+ tags: ['nuxt', 'feeds'],
28
+ },
29
+ ]
30
+
31
+ describe('feed format serializers', () => {
32
+ it('serializes RSS output with the stylesheet and item data', () => {
33
+ const xml = toRSS(items, config)
34
+
35
+ expect(xml).toContain('<?xml-stylesheet type="text/xsl" href="/feed/style.xsl"?>')
36
+ expect(xml).toContain('<title>Nuxt Layers Feed</title>')
37
+ expect(xml).toContain('Ada Lovelace')
38
+ expect(xml).toContain('/posts/a-b')
39
+ })
40
+
41
+ it('serializes JSON Feed output as a structured object', () => {
42
+ expect(toJSONFeed(items, config)).toMatchObject({
43
+ version: 'https://jsonfeed.org/version/1.1',
44
+ title: 'Nuxt Layers Feed',
45
+ description: 'Layered syndication output.',
46
+ home_page_url: 'https://example.com',
47
+ feed_url: 'https://example.com/feed/json',
48
+ authors: [{ name: 'Kieran Mansfield', url: 'https://example.com/about' }],
49
+ items: [
50
+ {
51
+ id: 'https://example.com/posts/a-b',
52
+ url: 'https://example.com/posts/a-b',
53
+ title: 'A & B',
54
+ content_text: 'Read > write',
55
+ date_published: '2026-01-03T10:00:00.000Z',
56
+ authors: [{ name: 'Ada Lovelace' }],
57
+ tags: ['nuxt', 'feeds'],
58
+ },
59
+ ],
60
+ })
61
+ })
62
+
63
+ it('serializes Atom output with escaped markup', () => {
64
+ const xml = toAtom(items, config)
65
+
66
+ expect(xml).toContain('<?xml-stylesheet type="text/xsl" href="/feed/style.xsl"?>')
67
+ expect(xml).toContain('<title>Nuxt Layers Feed</title>')
68
+ expect(xml).toContain('<title>A &amp; B</title>')
69
+ expect(xml).toContain('<summary>Read &gt; write</summary>')
70
+ })
71
+ })
@@ -1,6 +1,12 @@
1
1
  <script setup lang="ts">
2
2
  import { fieldConfigs, type FieldType } from '../../config/fields'
3
3
  import type { FieldSize } from '../../types/fields'
4
+ import {
5
+ buildBaseInputProps,
6
+ buildFormFieldProps,
7
+ buildNumberInputProps,
8
+ buildTextInputProps,
9
+ } from '../../utils/fieldProps'
4
10
 
5
11
  const {
6
12
  type = 'text',
@@ -40,36 +46,6 @@
40
46
  const isTextarea = computed(() => config.value.component === 'UTextarea')
41
47
  const isNumber = computed(() => config.value.component === 'UInputNumber')
42
48
 
43
- const formFieldProps = computed(() => ({
44
- name,
45
- required,
46
- size,
47
- ...(label !== undefined && { label }),
48
- ...(className !== undefined && { class: className }),
49
- }))
50
-
51
- const baseInputProps = computed(() => ({
52
- size,
53
- ...(resolvedPlaceholder.value !== undefined && { placeholder: resolvedPlaceholder.value }),
54
- ...(resolvedIcon.value !== undefined && { leadingIcon: resolvedIcon.value }),
55
- }))
56
-
57
- const numberInputProps = computed(() => ({
58
- size,
59
- ...(resolvedPlaceholder.value !== undefined && { placeholder: resolvedPlaceholder.value }),
60
- ...(resolvedIcon.value !== undefined && { leadingIcon: resolvedIcon.value }),
61
- ...(currencyOptions.value !== undefined && { formatOptions: currencyOptions.value }),
62
- }))
63
-
64
- const textInputProps = computed(() => ({
65
- type: config.value.inputType,
66
- size,
67
- ...(config.value.inputMode !== undefined && { inputmode: config.value.inputMode }),
68
- ...(config.value.autocomplete !== undefined && { autocomplete: config.value.autocomplete }),
69
- ...(resolvedPlaceholder.value !== undefined && { placeholder: resolvedPlaceholder.value }),
70
- ...(resolvedIcon.value !== undefined && { leadingIcon: resolvedIcon.value }),
71
- }))
72
-
73
49
  const currencyOptions = computed((): Intl.NumberFormatOptions | undefined => {
74
50
  if (config.value.format === 'currency') {
75
51
  return {
@@ -79,6 +55,42 @@
79
55
  }
80
56
  return undefined
81
57
  })
58
+
59
+ const formFieldProps = computed(() =>
60
+ buildFormFieldProps({
61
+ name,
62
+ required,
63
+ size,
64
+ label,
65
+ className,
66
+ })
67
+ )
68
+
69
+ const baseInputProps = computed(() =>
70
+ buildBaseInputProps({
71
+ size,
72
+ placeholder: resolvedPlaceholder.value,
73
+ icon: resolvedIcon.value,
74
+ })
75
+ )
76
+
77
+ const numberInputProps = computed(() =>
78
+ buildNumberInputProps({
79
+ size,
80
+ placeholder: resolvedPlaceholder.value,
81
+ icon: resolvedIcon.value,
82
+ formatOptions: currencyOptions.value,
83
+ })
84
+ )
85
+
86
+ const textInputProps = computed(() =>
87
+ buildTextInputProps({
88
+ config: config.value,
89
+ size,
90
+ placeholder: resolvedPlaceholder.value,
91
+ icon: resolvedIcon.value,
92
+ })
93
+ )
82
94
  </script>
83
95
 
84
96
  <template>
@@ -0,0 +1,65 @@
1
+ import type { FieldConfig } from '../types/fields'
2
+ import type { FieldSize } from '../types/fields'
3
+
4
+ export function buildFormFieldProps(args: {
5
+ name: string
6
+ required: boolean
7
+ size: FieldSize
8
+ label?: string | undefined
9
+ className?: string | undefined
10
+ }) {
11
+ const { name, required, size, label, className } = args
12
+ return {
13
+ name,
14
+ required,
15
+ size,
16
+ ...(label !== undefined && { label }),
17
+ ...(className !== undefined && { class: className }),
18
+ }
19
+ }
20
+
21
+ export function buildBaseInputProps(args: {
22
+ size: FieldSize
23
+ placeholder?: string | undefined
24
+ icon?: string | undefined
25
+ }) {
26
+ const { size, placeholder, icon } = args
27
+ return {
28
+ size,
29
+ ...(placeholder !== undefined && { placeholder }),
30
+ ...(icon !== undefined && { leadingIcon: icon }),
31
+ }
32
+ }
33
+
34
+ // fallow-ignore-next-line complexity
35
+ export function buildTextInputProps(args: {
36
+ config: FieldConfig
37
+ size: FieldSize
38
+ placeholder?: string | undefined
39
+ icon?: string | undefined
40
+ }) {
41
+ const { config, size, placeholder, icon } = args
42
+ return {
43
+ type: config.inputType,
44
+ size,
45
+ ...(config.inputMode !== undefined && { inputmode: config.inputMode }),
46
+ ...(config.autocomplete !== undefined && { autocomplete: config.autocomplete }),
47
+ ...(placeholder !== undefined && { placeholder }),
48
+ ...(icon !== undefined && { leadingIcon: icon }),
49
+ }
50
+ }
51
+
52
+ export function buildNumberInputProps(args: {
53
+ size: FieldSize
54
+ placeholder?: string | undefined
55
+ icon?: string | undefined
56
+ formatOptions?: Intl.NumberFormatOptions | undefined
57
+ }) {
58
+ const { size, placeholder, icon, formatOptions } = args
59
+ return {
60
+ size,
61
+ ...(placeholder !== undefined && { placeholder }),
62
+ ...(icon !== undefined && { leadingIcon: icon }),
63
+ ...(formatOptions !== undefined && { formatOptions }),
64
+ }
65
+ }