@syncedco/flow 0.4.1 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 - 2026-09-25
4
+
5
+ - Fix `border`, `border-t`, `border-2` and the other border width utilities
6
+ drawing nothing: they now also set `border-style: solid`.
7
+ - Fix `space-x-*`, `space-y-*`, `divide-x` and `divide-y` doing nothing unless
8
+ core CSS was included. They now generate their own child rules for any
9
+ value, so `space-y-7` works too.
10
+ - Fix `h-screen`, `min-h-screen`, `max-h-screen`, `top-screen`,
11
+ `bottom-screen` and `translate-y-screen` using the viewport width (`100svw`)
12
+ instead of the viewport height (`100svh`). `size-screen` now sets
13
+ `100svw` by `100svh`.
14
+ - Add `dvh`, `dvw`, `lvh` and `lvw` sizing keywords, such as `min-h-dvh`.
15
+ - Add common utilities: `border-dashed`/`dotted`/`double`/`none`,
16
+ `divide-<colour>`, `divide-dashed`, `divide-x-2`, `order-*`,
17
+ `col-start-*`/`col-end-*`/`row-start-*`/`row-end-*`, `justify-items-*`,
18
+ `justify-self-*`, `flex-initial`, `max-w-none`, `max-h-none`,
19
+ `object-<position>`, `bg-cover`/`contain`, `bg-<position>`,
20
+ `bg-no-repeat` and other repeat values, `bg-fixed`, `bg-linear-to-*`,
21
+ `bg-none`, `origin-*`, `font-serif`, `text-ellipsis`, `text-clip`, `blur`,
22
+ `transition-opacity`, `transition-shadow`, `transition-none`,
23
+ `animate-ping`, `animate-bounce` and `animate-none`.
24
+ - Improve `synced-flow lint` hints: suggest Flow's own class first
25
+ (`prose` suggests `sf-prose`), suggest the same utility with a valid value
26
+ (`h-scren` suggests `h-screen`, not `hidden`), and only suggest near typos,
27
+ so hints no longer name a class that does something else.
28
+
3
29
  ## 0.4.1 - 2026-09-09
4
30
 
5
31
  - Fix source-tree generated-CSS checks when the package is tested from a
@@ -3618,6 +3618,12 @@ function rawDeclarationsFor(base) {
3618
3618
  [/^overflow-x-(hidden|visible|auto|scroll|clip)$/, 'overflow-x'],
3619
3619
  [/^overflow-y-(hidden|visible|auto|scroll|clip)$/, 'overflow-y'],
3620
3620
  [/^object-(cover|contain|fill|none|scale-down)$/, 'object-fit'],
3621
+ [/^object-(center|top|bottom|left|right|left-top|left-bottom|right-top|right-bottom)$/, 'object-position'],
3622
+ [/^bg-(cover|contain|auto)$/, 'background-size'],
3623
+ [/^bg-(center|top|bottom|left|right|left-top|left-bottom|right-top|right-bottom)$/, 'background-position'],
3624
+ [/^bg-(repeat|no-repeat|repeat-x|repeat-y|repeat-round|repeat-space)$/, 'background-repeat'],
3625
+ [/^bg-(fixed|local|scroll)$/, 'background-attachment'],
3626
+ [/^origin-(center|top|bottom|left|right|top-left|top-right|bottom-left|bottom-right)$/, 'transform-origin'],
3621
3627
  [/^whitespace-(normal|nowrap|pre|pre-line|pre-wrap|break-spaces)$/, 'white-space'],
3622
3628
  [/^break-(normal|words|all|keep)$/, 'overflow-wrap'],
3623
3629
  [/^cursor-(pointer|default|not-allowed|grab|grabbing|text)$/, 'cursor'],
@@ -3627,11 +3633,17 @@ function rawDeclarationsFor(base) {
3627
3633
  ]
3628
3634
  for (const [pattern, property] of simpleMaps) {
3629
3635
  const match = base.match(pattern)
3630
- if (match) return [[property, match[1] === 'words' ? 'break-word' : match[1]]]
3636
+ if (match) {
3637
+ if (match[1] === 'words') return [[property, 'break-word']]
3638
+ // Positions are written `left-top` in the class and `left top` in CSS.
3639
+ return [[property, /-position$|-origin$/.test(property) ? match[1].replace('-', ' ') : match[1]]]
3640
+ }
3631
3641
  }
3632
3642
 
3633
3643
  if (base === 'appearance-none') return [['appearance', 'none']]
3634
3644
  if (base === 'resize') return [['resize', 'both']]
3645
+ if (base === 'text-ellipsis') return [['text-overflow', 'ellipsis']]
3646
+ if (base === 'text-clip') return [['text-overflow', 'clip']]
3635
3647
  if (base === 'truncate') return [['overflow', 'hidden'], ['text-overflow', 'ellipsis'], ['white-space', 'nowrap']]
3636
3648
  const lineClamp = base.match(/^line-clamp-(\d+)$/)
3637
3649
  if (lineClamp) return [['overflow', 'hidden'], ['display', '-webkit-box'], ['-webkit-box-orient', 'vertical'], ['-webkit-line-clamp', lineClamp[1]]]
@@ -3679,7 +3691,8 @@ function sideUtility(base, prefixes) {
3679
3691
  const match = base.match(new RegExp(`^-?${prefix}-(.+)$`))
3680
3692
  if (!match) continue
3681
3693
  const negative = base.startsWith('-')
3682
- const value = spacingUnit(match[1]) ?? sizeKeyword(match[1])
3694
+ const axis = prefix === 'top' || prefix === 'bottom' ? 'y' : 'x'
3695
+ const value = spacingUnit(match[1]) ?? sizeKeyword(match[1], axis)
3683
3696
  if (!value) continue
3684
3697
  const resolved = negative ? `calc(${value} * -1)` : value
3685
3698
  if (prefix === 'inset') return [['inset', resolved]]
@@ -3720,18 +3733,22 @@ function spacingUtility(base) {
3720
3733
  'gap-x': ['column-gap'],
3721
3734
  'gap-y': ['row-gap'],
3722
3735
  }
3723
- if (prefix === 'space-x') return [['--sf-space-x', resolved]]
3724
- if (prefix === 'space-y') return [['--sf-space-y', resolved]]
3736
+ if (prefix === 'space-x') return [['margin-inline-start', resolved]]
3737
+ if (prefix === 'space-y') return [['margin-block-start', resolved]]
3725
3738
  return map[prefix].map((property) => [property, resolved])
3726
3739
  }
3727
3740
 
3728
- function sizeKeyword(valueName) {
3729
- const map = {
3741
+ // Keyword sizes. `screen` is swapped for the viewport height on the y axis.
3742
+ const sizeKeywords = {
3730
3743
  auto: 'auto',
3731
3744
  full: '100%',
3732
3745
  screen: '100svw',
3733
3746
  svw: '100svw',
3734
3747
  svh: '100svh',
3748
+ dvw: '100dvw',
3749
+ dvh: '100dvh',
3750
+ lvw: '100lvw',
3751
+ lvh: '100lvh',
3735
3752
  min: 'min-content',
3736
3753
  max: 'max-content',
3737
3754
  fit: 'fit-content',
@@ -3758,15 +3775,21 @@ function sizeKeyword(valueName) {
3758
3775
  '4/5': '80%',
3759
3776
  '1/6': '16.666667%',
3760
3777
  '5/6': '83.333333%',
3761
- }
3762
- return map[valueName] ?? null
3778
+ }
3779
+
3780
+ function sizeKeyword(valueName, axis = 'x') {
3781
+ if (valueName === 'screen' && axis === 'y') return '100svh'
3782
+ return Object.hasOwn(sizeKeywords, valueName) ? sizeKeywords[valueName] : null
3763
3783
  }
3764
3784
 
3765
3785
  function sizingUtility(base) {
3766
3786
  const match = base.match(/^(w|h|min-w|min-h|max-w|max-h|size|basis)-(.+)$/)
3767
3787
  if (!match) return null
3768
3788
  const [, prefix, valueName] = match
3769
- const value = spacingUnit(valueName) ?? sizeKeyword(valueName)
3789
+ if ((prefix === 'max-w' || prefix === 'max-h') && valueName === 'none') return [[prefix === 'max-w' ? 'max-width' : 'max-height', 'none']]
3790
+ if (prefix === 'size' && valueName === 'screen') return [['width', '100svw'], ['height', '100svh']]
3791
+ const axis = prefix === 'h' || prefix === 'min-h' || prefix === 'max-h' ? 'y' : 'x'
3792
+ const value = spacingUnit(valueName) ?? sizeKeyword(valueName, axis)
3770
3793
  if (!value) return null
3771
3794
  const props = {
3772
3795
  w: ['width'],
@@ -3792,6 +3815,7 @@ function flexGridUtility(base) {
3792
3815
  'flex-1': [['flex', '1 1 0%']],
3793
3816
  'flex-auto': [['flex', '1 1 auto']],
3794
3817
  'flex-none': [['flex', 'none']],
3818
+ 'flex-initial': [['flex', '0 1 auto']],
3795
3819
  grow: [['flex-grow', '1']],
3796
3820
  'grow-0': [['flex-grow', '0']],
3797
3821
  shrink: [['flex-shrink', '1']],
@@ -3818,6 +3842,12 @@ function flexGridUtility(base) {
3818
3842
  const placeMap = { center: 'center', start: 'start', end: 'end', stretch: 'stretch' }
3819
3843
  let match = base.match(/^items-(start|end|center|baseline|stretch)$/)
3820
3844
  if (match) return [['align-items', alignMap[match[1]]]]
3845
+ match = base.match(/^justify-items-(start|end|center|stretch)$/)
3846
+ if (match) return [['justify-items', match[1]]]
3847
+ match = base.match(/^justify-self-(auto|start|end|center|stretch)$/)
3848
+ if (match) return [['justify-self', match[1]]]
3849
+ match = base.match(/^order-(\d+|first|last|none)$/)
3850
+ if (match) return [['order', { first: '-9999', last: '9999', none: '0' }[match[1]] ?? match[1]]]
3821
3851
  match = base.match(/^justify-(start|end|center|between|around|evenly|stretch)$/)
3822
3852
  if (match) return [['justify-content', justifyMap[match[1]]]]
3823
3853
  match = base.match(/^content-(start|end|center|between|around|evenly|stretch)$/)
@@ -3836,6 +3866,8 @@ function flexGridUtility(base) {
3836
3866
  match = base.match(/^row-span-(\d+)$/)
3837
3867
  if (match) return [['grid-row', `span ${match[1]} / span ${match[1]}`]]
3838
3868
  if (base === 'col-span-full') return [['grid-column', '1 / -1']]
3869
+ match = base.match(/^(col|row)-(start|end)-(\d+|auto)$/)
3870
+ if (match) return [[`grid-${match[1] === 'col' ? 'column' : 'row'}-${match[2]}`, match[3]]]
3839
3871
  if (base === 'grid-flow-col') return [['grid-auto-flow', 'column']]
3840
3872
  if (base === 'grid-flow-row') return [['grid-auto-flow', 'row']]
3841
3873
 
@@ -3843,10 +3875,11 @@ function flexGridUtility(base) {
3843
3875
  }
3844
3876
 
3845
3877
  function typographyUtility(base) {
3846
- let match = base.match(/^font-(sans|display|mono)$/)
3878
+ let match = base.match(/^font-(sans|serif|display|mono)$/)
3847
3879
  if (match) {
3848
3880
  const map = {
3849
3881
  sans: 'var(--font-inter, var(--sf-font-sans, ui-sans-serif, system-ui, sans-serif))',
3882
+ serif: 'var(--font-serif, var(--sf-font-serif, ui-serif, Georgia, Cambria, "Times New Roman", serif))',
3850
3883
  display: 'var(--font-display, var(--sf-font-display, ui-serif, Georgia, serif))',
3851
3884
  mono: 'var(--font-mono, var(--sf-font-mono, ui-monospace, monospace))',
3852
3885
  }
@@ -3894,22 +3927,33 @@ function colourUtility(base) {
3894
3927
  }
3895
3928
 
3896
3929
  function borderUtility(base) {
3897
- if (base === 'border') return [['border-width', '1px']]
3930
+ // A border width draws nothing without a line style, so width utilities set both.
3931
+ if (base === 'border') return [['border-width', '1px'], ['border-style', 'solid']]
3898
3932
  if (base === 'border-0') return [['border-width', '0']]
3899
- if (base === 'border-2') return [['border-width', '0.125rem']]
3900
- if (base === 'border-t') return [['border-top-width', '1px']]
3901
- if (base === 'border-r') return [['border-right-width', '1px']]
3902
- if (base === 'border-b') return [['border-bottom-width', '1px']]
3903
- if (base === 'border-l') return [['border-left-width', '1px']]
3904
- if (base === 'border-y') return [['border-top-width', '1px'], ['border-bottom-width', '1px']]
3905
- if (base === 'border-x') return [['border-left-width', '1px'], ['border-right-width', '1px']]
3933
+ if (base === 'border-2') return [['border-width', '0.125rem'], ['border-style', 'solid']]
3934
+ if (base === 'border-t') return [['border-top-width', '1px'], ['border-top-style', 'solid']]
3935
+ if (base === 'border-r') return [['border-right-width', '1px'], ['border-right-style', 'solid']]
3936
+ if (base === 'border-b') return [['border-bottom-width', '1px'], ['border-bottom-style', 'solid']]
3937
+ if (base === 'border-l') return [['border-left-width', '1px'], ['border-left-style', 'solid']]
3938
+ if (base === 'border-y') return [['border-top-width', '1px'], ['border-bottom-width', '1px'], ['border-top-style', 'solid'], ['border-bottom-style', 'solid']]
3939
+ if (base === 'border-x') return [['border-left-width', '1px'], ['border-right-width', '1px'], ['border-left-style', 'solid'], ['border-right-style', 'solid']]
3906
3940
  const sideWidth = base.match(/^border-(t|r|b|l)-(\d+)$/)
3907
3941
  if (sideWidth) {
3908
3942
  const side = { t: 'top', r: 'right', b: 'bottom', l: 'left' }[sideWidth[1]]
3909
- return [[`border-${side}-width`, borderWidthUnit(sideWidth[2])]]
3910
- }
3911
- if (base === 'divide-y') return [['--sf-divide-y', '1px']]
3912
- if (base === 'divide-x') return [['--sf-divide-x', '1px']]
3943
+ return [[`border-${side}-width`, borderWidthUnit(sideWidth[2])], [`border-${side}-style`, 'solid']]
3944
+ }
3945
+ const borderStyle = base.match(/^border-(solid|dashed|dotted|double|hidden|none)$/)
3946
+ if (borderStyle) return [['border-style', borderStyle[1]]]
3947
+ const divide = base.match(/^divide-(x|y)(?:-(\d+))?$/)
3948
+ if (divide) {
3949
+ const side = divide[1] === 'y' ? 'block' : 'inline'
3950
+ const width = divide[2] === undefined ? '1px' : borderWidthUnit(divide[2])
3951
+ return [[`border-${side}-start-width`, width], [`border-${side}-start-style`, 'solid']]
3952
+ }
3953
+ const divideStyle = base.match(/^divide-(solid|dashed|dotted|double|none)$/)
3954
+ if (divideStyle) return [['border-style', divideStyle[1]]]
3955
+ const divideColour = base.match(/^divide-(.+)$/)
3956
+ if (divideColour && colourValue(divideColour[1])) return [['border-color', colourValue(divideColour[1])]]
3913
3957
  const roundedSide = base.match(/^rounded-(t|r|b|l|tr|tl|br|bl)-(none|xs|sm|md|lg|xl|2xl|3xl|full)$/)
3914
3958
  if (roundedSide) {
3915
3959
  const radius = `var(--radius-${roundedSide[2]})`
@@ -3947,6 +3991,9 @@ function borderUtility(base) {
3947
3991
  }
3948
3992
 
3949
3993
  function effectUtility(base) {
3994
+ const linear = base.match(/^bg-linear-to-(t|b|l|r|tl|tr|bl|br)$/)
3995
+ if (linear) return effectUtility(`bg-gradient-to-${linear[1]}`)
3996
+ if (base === 'bg-none') return [['background-image', 'none']]
3950
3997
  if (base === 'bg-gradient-to-r') return [['background-image', 'linear-gradient(to right, var(--sf-gradient-stops))']]
3951
3998
  if (base === 'bg-gradient-to-l') return [['background-image', 'linear-gradient(to left, var(--sf-gradient-stops))']]
3952
3999
  if (base === 'bg-gradient-to-t') return [['background-image', 'linear-gradient(to top, var(--sf-gradient-stops))']]
@@ -3968,6 +4015,7 @@ function effectUtility(base) {
3968
4015
 
3969
4016
  let match = base.match(/^opacity-(\d+)$/)
3970
4017
  if (match) return [['opacity', String(Number(match[1]) / 100)]]
4018
+ if (base === 'blur') return [['filter', 'blur(0.5rem)']]
3971
4019
  match = base.match(/^blur-(none|sm|md|lg|xl|2xl|3xl)$/)
3972
4020
  if (match) {
3973
4021
  const map = { none: '0', sm: '0.25rem', md: '0.75rem', lg: '1rem', xl: '1.5rem', '2xl': '2.5rem', '3xl': '4rem' }
@@ -3998,6 +4046,9 @@ function effectUtility(base) {
3998
4046
  if (base === 'transition') return [['transition-property', 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
3999
4047
  if (base === 'transition-all') return [['transition-property', 'all'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
4000
4048
  if (base === 'transition-colors') return [['transition-property', 'color, background-color, border-color, text-decoration-color, fill, stroke'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
4049
+ if (base === 'transition-opacity') return [['transition-property', 'opacity'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
4050
+ if (base === 'transition-shadow') return [['transition-property', 'box-shadow'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
4051
+ if (base === 'transition-none') return [['transition-property', 'none']]
4001
4052
  if (base === 'transition-transform') return [['transition-property', 'transform'], ['transition-duration', 'var(--duration-normal)'], ['transition-timing-function', 'var(--ease-standard)']]
4002
4053
  match = base.match(/^transition-\[(.+)\]$/)
4003
4054
  if (match) return [['transition-property', arbitraryValue(match[1])], ['transition-duration', 'var(--duration-normal)']]
@@ -4010,6 +4061,9 @@ function effectUtility(base) {
4010
4061
  if (base === 'ease-in-out') return [['transition-timing-function', 'cubic-bezier(0.4, 0, 0.2, 1)']]
4011
4062
  if (base === 'animate-pulse') return [['animation', 'sf-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite']]
4012
4063
  if (base === 'animate-spin') return [['animation', 'sf-spin 1s linear infinite']]
4064
+ if (base === 'animate-ping') return [['animation', 'sf-ping 1s cubic-bezier(0, 0, 0.2, 1) infinite']]
4065
+ if (base === 'animate-bounce') return [['animation', 'sf-bounce 1s infinite']]
4066
+ if (base === 'animate-none') return [['animation', 'none']]
4013
4067
  if (base === 'animate-in') return [['animation', 'sf-enter var(--duration-normal) var(--ease-standard) both']]
4014
4068
  if (base === 'animate-out') return [['animation', 'sf-exit var(--duration-fast) var(--ease-standard) both']]
4015
4069
  return null
@@ -4020,7 +4074,7 @@ function transformUtility(base) {
4020
4074
  let match = base.match(/^(-?)translate-x-(.+)$/)
4021
4075
  if (match) declarations.push(['--sf-translate-x', `${match[1] ? '-' : ''}${spacingUnit(match[2]) ?? sizeKeyword(match[2])}`])
4022
4076
  match = base.match(/^(-?)translate-y-(.+)$/)
4023
- if (match) declarations.push(['--sf-translate-y', `${match[1] ? '-' : ''}${spacingUnit(match[2]) ?? sizeKeyword(match[2])}`])
4077
+ if (match) declarations.push(['--sf-translate-y', `${match[1] ? '-' : ''}${spacingUnit(match[2]) ?? sizeKeyword(match[2], 'y')}`])
4024
4078
  match = base.match(/^scale-\[(.+)\]$/)
4025
4079
  if (match) declarations.push(['--sf-scale-x', arbitraryValue(match[1])], ['--sf-scale-y', arbitraryValue(match[1])])
4026
4080
  match = base.match(/^scale-(\d+)$/)
@@ -4045,7 +4099,9 @@ function cssRuleFor(token) {
4045
4099
  const selectorResult = selectorFor(token, variants)
4046
4100
  if (!selectorResult) return null
4047
4101
 
4048
- const { selector, wrappers, needsPseudoContent } = selectorResult
4102
+ const { wrappers, needsPseudoContent } = selectorResult
4103
+ // space-* and divide-* style the gaps between children, not the element itself.
4104
+ const selector = /^-?(space-[xy]-|divide-)/.test(base) ? `${selectorResult.selector} > * + *` : selectorResult.selector
4049
4105
  const allDeclarations = needsPseudoContent ? [['content', '""'], ...declarations] : declarations
4050
4106
  let rule = `${selector}{${allDeclarations.map(([property, value]) => `${property}:${value}`).join(';')}}`
4051
4107
 
@@ -4713,16 +4769,6 @@ function buildLayoutCss() {
4713
4769
  }
4714
4770
  }
4715
4771
 
4716
- :where(.space-y-1, .space-y-2, .space-y-3, .space-y-4, .space-y-5, .space-y-6, .space-y-8, .space-y-10, .space-y-12) > * + * {
4717
- margin-block-start: var(--sf-space-y);
4718
- }
4719
-
4720
- :where(.space-x-1, .space-x-2, .space-x-3, .space-x-4, .space-x-5, .space-x-6, .space-x-8) > * + * {
4721
- margin-inline-start: var(--sf-space-x);
4722
- }
4723
-
4724
- .divide-y > * + * { border-block-start-width: var(--sf-divide-y); }
4725
- .divide-x > * + * { border-inline-start-width: var(--sf-divide-x); }
4726
4772
  }`
4727
4773
  }
4728
4774
 
@@ -5550,6 +5596,8 @@ function buildUsedKeyframesCss(bases) {
5550
5596
 
5551
5597
  if (bases.has('animate-pulse')) keyframes.push('@keyframes sf-pulse { 50% { opacity: .5; } }')
5552
5598
  if (bases.has('animate-spin')) keyframes.push('@keyframes sf-spin { to { transform: rotate(360deg); } }')
5599
+ if (bases.has('animate-ping')) keyframes.push('@keyframes sf-ping { 75%, 100% { transform: scale(2); opacity: 0; } }')
5600
+ if (bases.has('animate-bounce')) keyframes.push('@keyframes sf-bounce { 0%, 100% { transform: translateY(-25%); animation-timing-function: cubic-bezier(0.8, 0, 1, 1); } 50% { transform: none; animation-timing-function: cubic-bezier(0, 0, 0.2, 1); } }')
5553
5601
  if (bases.has('animate-in')) keyframes.push('@keyframes sf-enter { from { opacity: 0; transform: translate3d(0, .5rem, 0) scale(.98); } to { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } }')
5554
5602
  if (bases.has('animate-out')) keyframes.push('@keyframes sf-exit { from { opacity: 1; transform: translate3d(0, 0, 0) scale(1); } to { opacity: 0; transform: translate3d(0, .25rem, 0) scale(.98); } }')
5555
5603
 
@@ -5591,11 +5639,25 @@ function utilitySuggestionClasses() {
5591
5639
 
5592
5640
  function nearestClass(token) {
5593
5641
  const { base } = splitVariants(token)
5594
- const candidates = knownPublicClasses()
5595
- const scored = candidates
5642
+ const known = knownPublicClasses()
5643
+ // Flow's own version of a common class, e.g. `prose` -> `sf-prose`.
5644
+ if (known.includes(`sf-${base}`)) return `sf-${base}`
5645
+ const scored = [...siblingUtilities(base), ...known]
5596
5646
  .map((candidate) => ({ candidate, score: levenshtein(base, candidate) }))
5597
5647
  .sort((a, b) => a.score - b.score)
5598
- return scored[0]?.score <= Math.max(4, Math.ceil(base.length / 3)) ? scored[0].candidate : null
5648
+ // Only near typos: a looser match often names a class that does something else.
5649
+ return scored[0]?.score <= Math.max(1, Math.floor(base.length / 5)) ? scored[0].candidate : null
5650
+ }
5651
+
5652
+ // The same utility with other keyword or spacing values, e.g. `h-scren` -> `h-screen`.
5653
+ function siblingUtilities(base) {
5654
+ const cut = base.lastIndexOf('-')
5655
+ if (cut <= 0) return []
5656
+ const prefix = base.slice(0, cut)
5657
+ const values = [...Object.keys(sizeKeywords), 'none', 'first', 'last', 'center', 'start', 'end', '0', 'px', '1', '2', '3', '4', '6', '8', '10', '12', '16', '24']
5658
+ return values
5659
+ .map((value) => `${prefix}-${value}`)
5660
+ .filter((candidate) => candidate !== base && declarationsFor(candidate))
5599
5661
  }
5600
5662
 
5601
5663
  function lintSourceFiles() {
@@ -602,16 +602,6 @@
602
602
  }
603
603
  }
604
604
 
605
- :where(.space-y-1, .space-y-2, .space-y-3, .space-y-4, .space-y-5, .space-y-6, .space-y-8, .space-y-10, .space-y-12) > * + * {
606
- margin-block-start: var(--sf-space-y);
607
- }
608
-
609
- :where(.space-x-1, .space-x-2, .space-x-3, .space-x-4, .space-x-5, .space-x-6, .space-x-8) > * + * {
610
- margin-inline-start: var(--sf-space-x);
611
- }
612
-
613
- .divide-y > * + * { border-block-start-width: var(--sf-divide-y); }
614
- .divide-x > * + * { border-inline-start-width: var(--sf-divide-x); }
615
605
  }
616
606
 
617
607
  @layer components {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncedco/flow",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "AI-friendly fluid CSS design system for brand-consistent websites and agency builds.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.2.2",