package tw

  1. Overview
  2. Docs
Legend:
Page
Library
Module
Module type
Parameter
Class
Class type
Source

Source file sizing.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
(** Sizing utilities for width and height *)

module Css = Cascade.Css

module Handler = struct
  let class_float = Pp.float

  open Style
  open Css

  (** The property a sizing family sets. [Width_and_height] is [size-*], which
      sets both. *)
  type prop =
    | Width
    | Height
    | Min_width
    | Min_height
    | Max_width
    | Max_height
    | Width_and_height
    | Inline_size
    | Min_inline_size
    | Max_inline_size
    | Block_size
    | Min_block_size
    | Max_block_size

  (** The value a sizing class carries. [Keyword] and [Themed] come from the
      family's own table, so they carry their class spelling and their suborder
      with them; a [Themed] value also carries the theme-variable declaration
      and the [var()] reference the utility emits. *)
  type value =
    | Keyword of { spelling : string; length : length; order : int }
    | Themed of {
        spelling : string;
        decl : declaration;
        value : length;
        order : int;
      }
    | Spacing of float
    | Fraction of string
    | Arbitrary of string * length
    | Arbitrary_raw of string * string
        (** a bracket no length reader took: the family names its longhands, so
            the value is forwarded verbatim - Tailwind's token-stream contract
        *)
    | Theme_named of string  (** a size the project named in its own [@theme] *)

  type t =
    | Sized of prop * value
    | Aspect_auto
    | Aspect_square
    | Aspect_video
    | Aspect_ratio of float * float (* aspect-4/3, aspect-8.5/11 *)
    | Aspect_arbitrary of string * string
      (* aspect-[16/9]: the bracket as written, and the value it declares *)
    | Aspect_theme of string (* ratio named by a project [--aspect-*] token *)

  let name = "sizing"

  (* Tailwind spacing order helper: matches canonical spacing scale order. n is
     the rem value, so we convert to class units (multiply by 4). *)
  let spacing_suborder n =
    let class_units = n *. 4. in
    let integer = int_of_float (floor class_units) in
    let frac = class_units -. float_of_int integer in
    (* Fractional part order: .0, .5, .25, .75 (not numeric!) *)
    let frac_order =
      if frac = 0.0 then 0
      else if abs_float (frac -. 0.5) < 0.01 then 1
      else if abs_float (frac -. 0.25) < 0.01 then 2
      else if abs_float (frac -. 0.75) < 0.01 then 3
      else 4 (* fallback for other values *)
    in
    (integer * 4) + frac_order

  (* Tailwind interleaves spacing values and fractions within a sizing family by
     the integer part of their magnitude (spacing value or fraction numerator):
     e.g. w-0.5, w-1, w-1.5, w-1/2, w-1/3, w-2, w-2/3, w-3/4. Spacing sorts by
     value, fractions of numerator n come just after the spacing values with
     that integer part, ordered by denominator. Both stay well under the
     per-family arbitrary offset (5_000_000). *)
  let spacing_value_order n = spacing_suborder n * 100

  (* A spacing value is stored as rem (class number * 0.25). A fraction n/m has
     numerator [n], whose class number is [n], so [spacing_suborder (n *. 0.25)]
     puts it on the spacing scale. [+ 4] steps to the next class boundary
     (spacing_suborder for an integer class k is 4k); [- 1] pulls every n/m into
     the last slot before that boundary, after every floor-n spacing value. The
     natural candidate tiebreak then orders the unbounded denominators. *)
  let fraction_value_order f =
    match Parse.fraction f with
    | Some (n, _) -> ((spacing_suborder (float_of_int n *. 0.25) + 4) * 100) - 1
    | None -> 490000

  (* Within a family: spacing and fractions interleaved by magnitude, then an
     arbitrary value, then the keywords. *)
  let arbitrary_off = 5_000_000
  let keyword_off = 6_000_000

  (* Family bases are 10M apart so the interleaved spacing/fraction range (< 5M)
     and the arbitrary/keyword offsets never overflow into the next family. *)
  let aspect_base = -10_000_000

  (* Within the dimension families, size-* sorts before h/w/max/min. *)
  let size_base = 0
  let h_base = 10_000_000
  let max_h_base = 20_000_000
  let min_h_base = 30_000_000
  let w_base = 40_000_000
  let max_w_base = 50_000_000
  let min_w_base = 60_000_000

  (* Tailwind registers the logical sizing utilities after every other one, so
     they sort last rather than beside w-* and h-*; [logical_priority] carries
     that. Their bases run in the alphabetical order Tailwind falls back to. *)
  let logical = 80_000_000
  let block_base = logical
  let inline_base = 90_000_000
  let max_block_base = 100_000_000
  let max_inline_base = 110_000_000
  let min_block_base = 120_000_000
  let min_inline_base = 130_000_000
  let logical_priority = 38

  (* A Tailwind sizing fraction [n/m] resolves to [n/m * 100%]: any numerator
     over any positive denominator, read as a percentage rather than off a fixed
     scale. *)
  let fraction_pct = Parse.fraction_pct

  (* Container size theme variables - ordered from smallest to largest *)
  let container_3xs = Var.theme Css.Length "container-3xs" ~order:(5, 0)
  let container_2xs = Var.theme Css.Length "container-2xs" ~order:(5, 1)
  let container_xs = Var.theme Css.Length "container-xs" ~order:(5, 2)
  let container_sm = Var.theme Css.Length "container-sm" ~order:(5, 3)
  let container_md = Var.theme Css.Length "container-md" ~order:(5, 4)
  let container_lg = Var.theme Css.Length "container-lg" ~order:(5, 5)
  let container_xl = Var.theme Css.Length "container-xl" ~order:(5, 6)
  let container_2xl = Var.theme Css.Length "container-2xl" ~order:(5, 7)
  let container_3xl = Var.theme Css.Length "container-3xl" ~order:(5, 8)
  let container_4xl = Var.theme Css.Length "container-4xl" ~order:(5, 9)
  let container_5xl = Var.theme Css.Length "container-5xl" ~order:(5, 10)
  let container_6xl = Var.theme Css.Length "container-6xl" ~order:(5, 11)
  let container_7xl = Var.theme Css.Length "container-7xl" ~order:(5, 12)

  (* The container scale doubles as the named width scale in v4. This is the
     only place its names, theme variables, defaults and order live. *)
  let container_scale =
    [
      ("3xs", container_3xs, (Rem 16.0 : length));
      ("2xs", container_2xs, Rem 18.0);
      ("xs", container_xs, Rem 20.0);
      ("sm", container_sm, Rem 24.0);
      ("md", container_md, Rem 28.0);
      ("lg", container_lg, Rem 32.0);
      ("xl", container_xl, Rem 36.0);
      ("2xl", container_2xl, Rem 42.0);
      ("3xl", container_3xl, Rem 48.0);
      ("4xl", container_4xl, Rem 56.0);
      ("5xl", container_5xl, Rem 64.0);
      ("6xl", container_6xl, Rem 72.0);
      ("7xl", container_7xl, Rem 80.0);
    ]

  let container_binding name =
    List.find_map
      (fun (n, var, default) -> if n = name then Some (var, default) else None)
      container_scale

  (* Publish the scale through the theme-token registry, the way rule.ml
     publishes the breakpoints, so [max-w-[theme(--container-md)]] resolves and
     [theme(static)] emits it. *)
  let () =
    List.iter
      (fun (_, var, default) -> Theme.register_default var default)
      container_scale

  (* Breakpoint theme vars, referenced by the (v3) max-w-screen-* utilities.
     Negative suborders keep them before --container-* in the theme layer, as
     Tailwind emits them. *)
  let breakpoint_sm = Var.theme Css.Length "breakpoint-sm" ~order:(5, -5)
  let breakpoint_md = Var.theme Css.Length "breakpoint-md" ~order:(5, -4)
  let breakpoint_lg = Var.theme Css.Length "breakpoint-lg" ~order:(5, -3)
  let breakpoint_xl = Var.theme Css.Length "breakpoint-xl" ~order:(5, -2)
  let breakpoint_2xl = Var.theme Css.Length "breakpoint-2xl" ~order:(5, -1)

  (* Table entries: [kw] resolves to a literal length, [themed] to a theme
     variable carrying its default. *)
  let kw spelling length order = Keyword { spelling; length; order }

  let themed spelling var default order =
    let decl, ref_ = Var.binding var default in
    Themed { spelling; decl; value = Var ref_; order }

  (* The whole container scale, in scale order, from suborder offset [off]. *)
  let container_entries off =
    List.mapi
      (fun i (spelling, var, default) -> themed spelling var default (off + i))
      container_scale

  (* The container scale in a bespoke suborder, given as name/offset pairs. *)
  let container_entries_by offs =
    List.filter_map
      (fun (spelling, var, default) ->
        Option.map
          (fun off -> themed spelling var default off)
          (List.assoc_opt spelling offs))
      container_scale

  type family = {
    prefix : string;  (** class prefix, e.g. ["min-w"] *)
    css_name : string;  (** property name, as error messages spell it *)
    decls : (length -> declaration) list;
    properties : string list;
        (** the longhands [decls] writes, for a value no typed setter can hold
        *)
    base : int;  (** suborder base for the family *)
    spacing_off : int;  (** extra offset in front of the spacing band *)
    entries : value list;  (** the keywords this family admits *)
  }

  let width_family =
    {
      prefix = "w";
      css_name = "width";
      decls = [ width ];
      properties = [ "width" ];
      base = w_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "dvw" (Dvw 100.) (keyword_off + 1);
          kw "dvh" (Dvh 100.) (keyword_off + 1);
          kw "lvh" (Lvh 100.) (keyword_off + 1);
          kw "svh" (Svh 100.) (keyword_off + 1);
          kw "fit" Fit_content (keyword_off + 2);
          kw "full" (Pct 100.0) (keyword_off + 3);
          kw "lvw" (Lvw 100.) (keyword_off + 4);
          kw "max" Max_content (keyword_off + 5);
          kw "min" Min_content (keyword_off + 6);
          kw "px" (Px 1.0) (keyword_off + 7);
          kw "screen" (Vw 100.0) (keyword_off + 8);
          kw "svw" (Svw 100.) (keyword_off + 9);
        ]
        @ container_entries (keyword_off + 10);
    }

  let height_family =
    {
      prefix = "h";
      css_name = "height";
      decls = [ height ];
      properties = [ "height" ];
      base = h_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "dvh" (Dvh 100.) (keyword_off + 1);
          kw "fit" Fit_content (keyword_off + 2);
          kw "full" (Pct 100.0) (keyword_off + 3);
          kw "lh" (Lh 1.) (keyword_off + 4);
          kw "lvh" (Lvh 100.) (keyword_off + 5);
          kw "max" Max_content (keyword_off + 6);
          kw "min" Min_content (keyword_off + 7);
          kw "px" (Px 1.0) (keyword_off + 8);
          kw "screen" (Vh 100.0) (keyword_off + 9);
          kw "svh" (Svh 100.) (keyword_off + 10);
          kw "dvw" (Dvw 100.) (keyword_off + 10);
          kw "lvw" (Lvw 100.) (keyword_off + 10);
          kw "svw" (Svw 100.) (keyword_off + 10);
        ];
    }

  let min_width_family =
    {
      prefix = "min-w";
      css_name = "min-width";
      decls = [ min_width ];
      properties = [ "min-width" ];
      base = min_w_base;
      spacing_off = 0;
      entries =
        [
          kw "0" (Px 0.) 0;
          kw "auto" Auto (keyword_off + 0);
          kw "fit" Fit_content (keyword_off + 1);
          kw "full" (Pct 100.0) (keyword_off + 2);
          kw "dvh" (Dvh 100.) (keyword_off + 2);
          kw "dvw" (Dvw 100.) (keyword_off + 2);
          kw "lvh" (Lvh 100.) (keyword_off + 2);
          kw "lvw" (Lvw 100.) (keyword_off + 2);
          kw "svh" (Svh 100.) (keyword_off + 2);
          kw "svw" (Svw 100.) (keyword_off + 2);
          kw "px" (Px 1.0) (keyword_off + 2);
          kw "screen" (Vw 100.0) (keyword_off + 2);
          kw "max" Max_content (keyword_off + 3);
          kw "min" Min_content (keyword_off + 4);
        ]
        @ container_entries (keyword_off + 5);
    }

  let min_height_family =
    {
      prefix = "min-h";
      css_name = "min-height";
      decls = [ min_height ];
      properties = [ "min-height" ];
      base = min_h_base;
      spacing_off = 0;
      entries =
        [
          kw "0" (Px 0.) 0;
          kw "auto" Auto (keyword_off + 0);
          kw "dvh" (Dvh 100.) (keyword_off + 1);
          kw "fit" Fit_content (keyword_off + 2);
          kw "full" (Pct 100.0) (keyword_off + 3);
          kw "lh" (Lh 1.) (keyword_off + 4);
          kw "lvh" (Lvh 100.) (keyword_off + 5);
          kw "max" Max_content (keyword_off + 6);
          kw "min" Min_content (keyword_off + 7);
          kw "screen" (Vh 100.0) (keyword_off + 8);
          kw "svh" (Svh 100.) (keyword_off + 9);
          kw "px" (Px 1.0) (keyword_off + 9);
          kw "dvw" (Dvw 100.) (keyword_off + 9);
          kw "lvw" (Lvw 100.) (keyword_off + 9);
          kw "svw" (Svw 100.) (keyword_off + 9);
        ];
    }

  (* Tailwind orders max-width in three bands: the container scale sizes
     (2xl..7xl) by number, then the numeric spacing values, then an arbitrary
     value, then the letter-prefixed keywords alphabetically. Only this family
     shifts its spacing band ([spacing_off]); its fractions stay put. *)
  let max_width_family =
    {
      prefix = "max-w";
      css_name = "max-width";
      decls = [ max_width ];
      properties = [ "max-width" ];
      base = max_w_base;
      spacing_off = 1_000_000;
      entries =
        [
          kw "dvh" (Dvh 100.) 0;
          kw "dvw" (Dvw 100.) 0;
          kw "lvh" (Lvh 100.) 0;
          kw "lvw" (Lvw 100.) 0;
          kw "svh" (Svh 100.) 0;
          kw "svw" (Svw 100.) 0;
          kw "px" (Px 1.0) 0;
          kw "screen" (Vw 100.0) 0;
          kw "fit" Fit_content (keyword_off + 0);
          kw "full" (Pct 100.0) (keyword_off + 1);
          kw "max" Max_content (keyword_off + 3);
          kw "min" Min_content (keyword_off + 5);
          kw "none" None (keyword_off + 6);
          kw "prose" (Ch 65.0) (keyword_off + 7);
          themed "screen-2xl" breakpoint_2xl (Rem 96.) (keyword_off + 8);
          themed "screen-lg" breakpoint_lg (Rem 64.) (keyword_off + 9);
          themed "screen-md" breakpoint_md (Rem 48.) (keyword_off + 10);
          themed "screen-sm" breakpoint_sm (Rem 40.) (keyword_off + 11);
          themed "screen-xl" breakpoint_xl (Rem 80.) (keyword_off + 12);
        ]
        @ container_entries_by
            [
              ("2xs", 0);
              ("3xs", 0);
              ("2xl", 0);
              ("3xl", 1);
              ("4xl", 2);
              ("5xl", 3);
              ("6xl", 4);
              ("7xl", 5);
              ("lg", keyword_off + 2);
              ("md", keyword_off + 4);
              ("sm", keyword_off + 13);
              ("xl", keyword_off + 14);
              ("xs", keyword_off + 15);
            ];
    }

  let max_height_family =
    {
      prefix = "max-h";
      css_name = "max-height";
      decls = [ max_height ];
      properties = [ "max-height" ];
      base = max_h_base;
      spacing_off = 0;
      entries =
        [
          kw "dvh" (Dvh 100.) (keyword_off + 0);
          kw "fit" Fit_content (keyword_off + 1);
          kw "full" (Pct 100.0) (keyword_off + 2);
          kw "lh" (Lh 1.) (keyword_off + 3);
          kw "lvh" (Lvh 100.) (keyword_off + 4);
          kw "max" Max_content (keyword_off + 5);
          kw "min" Min_content (keyword_off + 6);
          kw "none" None (keyword_off + 7);
          kw "screen" (Vh 100.0) (keyword_off + 8);
          kw "svh" (Svh 100.) (keyword_off + 9);
          kw "px" (Px 1.0) (keyword_off + 9);
          kw "dvw" (Dvw 100.) (keyword_off + 9);
          kw "lvw" (Lvw 100.) (keyword_off + 9);
          kw "svw" (Svw 100.) (keyword_off + 9);
        ];
    }

  let size_family =
    {
      prefix = "size";
      css_name = "size";
      decls = [ width; height ];
      properties = [ "width"; "height" ];
      base = size_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "fit" Fit_content (keyword_off + 1);
          kw "full" (Pct 100.0) (keyword_off + 2);
          kw "dvh" (Dvh 100.) (keyword_off + 2);
          kw "dvw" (Dvw 100.) (keyword_off + 2);
          kw "lvh" (Lvh 100.) (keyword_off + 2);
          kw "lvw" (Lvw 100.) (keyword_off + 2);
          kw "svh" (Svh 100.) (keyword_off + 2);
          kw "svw" (Svw 100.) (keyword_off + 2);
          kw "px" (Px 1.0) (keyword_off + 2);
          kw "max" Max_content (keyword_off + 3);
          kw "min" Min_content (keyword_off + 4);
        ];
    }

  let inline_family =
    {
      prefix = "inline";
      css_name = "inline-size";
      decls = [ inline_size ];
      properties = [ "inline-size" ];
      base = inline_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "dvw" (Dvw 100.) (keyword_off + 1);
          kw "fit" Fit_content (keyword_off + 2);
          kw "full" (Pct 100.) (keyword_off + 3);
          kw "lvw" (Lvw 100.) (keyword_off + 4);
          kw "max" Max_content (keyword_off + 5);
          kw "min" Min_content (keyword_off + 6);
          kw "screen" (Vw 100.) (keyword_off + 7);
          kw "svw" (Svw 100.) (keyword_off + 8);
          kw "px" (Px 1.) (keyword_off + 20);
        ]
        @ container_entries (keyword_off + 9);
    }

  let min_inline_family =
    {
      prefix = "min-inline";
      css_name = "min-inline-size";
      decls = [ min_inline_size ];
      properties = [ "min-inline-size" ];
      base = min_inline_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "fit" Fit_content (keyword_off + 1);
          kw "full" (Pct 100.) (keyword_off + 2);
          kw "screen" (Vw 100.0) (keyword_off + 2);
          kw "px" (Px 1.0) (keyword_off + 2);
          kw "svw" (Svw 100.) (keyword_off + 2);
          kw "lvw" (Lvw 100.) (keyword_off + 2);
          kw "dvw" (Dvw 100.) (keyword_off + 2);
          kw "max" Max_content (keyword_off + 3);
          kw "min" Min_content (keyword_off + 4);
        ]
        @ container_entries (keyword_off + 5);
    }

  let max_inline_family =
    {
      prefix = "max-inline";
      css_name = "max-inline-size";
      decls = [ max_inline_size ];
      properties = [ "max-inline-size" ];
      base = max_inline_base;
      spacing_off = 0;
      entries =
        [
          kw "fit" Fit_content (keyword_off + 0);
          kw "full" (Pct 100.) (keyword_off + 1);
          kw "min" Min_content (keyword_off + 1);
          kw "screen" (Vw 100.0) (keyword_off + 1);
          kw "px" (Px 1.0) (keyword_off + 1);
          kw "svw" (Svw 100.) (keyword_off + 1);
          kw "lvw" (Lvw 100.) (keyword_off + 1);
          kw "dvw" (Dvw 100.) (keyword_off + 1);
          kw "max" Max_content (keyword_off + 2);
          kw "none" None (keyword_off + 3);
        ]
        @ container_entries (keyword_off + 4);
    }

  let block_family =
    {
      prefix = "block";
      css_name = "block-size";
      decls = [ block_size ];
      properties = [ "block-size" ];
      base = block_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "dvh" (Dvh 100.) (keyword_off + 1);
          kw "fit" Fit_content (keyword_off + 2);
          kw "full" (Pct 100.) (keyword_off + 3);
          kw "lh" (Lh 1.) (keyword_off + 4);
          kw "lvh" (Lvh 100.) (keyword_off + 5);
          kw "max" Max_content (keyword_off + 6);
          kw "min" Min_content (keyword_off + 7);
          kw "screen" (Vh 100.) (keyword_off + 8);
          kw "svh" (Svh 100.) (keyword_off + 9);
          kw "px" (Px 1.) (keyword_off + 20);
        ];
    }

  let min_block_family =
    {
      prefix = "min-block";
      css_name = "min-block-size";
      decls = [ min_block_size ];
      properties = [ "min-block-size" ];
      base = min_block_base;
      spacing_off = 0;
      entries =
        [
          kw "auto" Auto (keyword_off + 0);
          kw "dvh" (Dvh 100.) (keyword_off + 1);
          kw "fit" Fit_content (keyword_off + 2);
          kw "full" (Pct 100.) (keyword_off + 3);
          kw "px" (Px 1.0) (keyword_off + 3);
          kw "lh" (Lh 1.) (keyword_off + 4);
          kw "lvh" (Lvh 100.) (keyword_off + 5);
          kw "max" Max_content (keyword_off + 6);
          kw "min" Min_content (keyword_off + 7);
          kw "screen" (Vh 100.) (keyword_off + 8);
          kw "svh" (Svh 100.) (keyword_off + 9);
        ];
    }

  let max_block_family =
    {
      prefix = "max-block";
      css_name = "max-block-size";
      decls = [ max_block_size ];
      properties = [ "max-block-size" ];
      base = max_block_base;
      spacing_off = 0;
      entries =
        [
          kw "dvh" (Dvh 100.) (keyword_off + 0);
          kw "fit" Fit_content (keyword_off + 1);
          kw "full" (Pct 100.) (keyword_off + 2);
          kw "px" (Px 1.0) (keyword_off + 2);
          kw "lh" (Lh 1.) (keyword_off + 3);
          kw "lvh" (Lvh 100.) (keyword_off + 4);
          kw "max" Max_content (keyword_off + 5);
          kw "min" Min_content (keyword_off + 6);
          kw "none" None (keyword_off + 7);
          kw "screen" (Vh 100.) (keyword_off + 8);
          kw "svh" (Svh 100.) (keyword_off + 9);
        ];
    }

  let family = function
    | Width -> width_family
    | Height -> height_family
    | Min_width -> min_width_family
    | Min_height -> min_height_family
    | Max_width -> max_width_family
    | Max_height -> max_height_family
    | Width_and_height -> size_family
    | Inline_size -> inline_family
    | Min_inline_size -> min_inline_family
    | Max_inline_size -> max_inline_family
    | Block_size -> block_family
    | Min_block_size -> min_block_family
    | Max_block_size -> max_block_family

  (* The part of the class name that follows the family prefix. *)
  let class_suffix = function
    | Keyword k -> k.spelling
    | Themed t -> t.spelling
    | Spacing n -> class_float (n *. 4.)
    | Fraction f -> f
    | Arbitrary (raw, _) | Arbitrary_raw (raw, _) -> "[" ^ raw ^ "]"
    | Theme_named spelling -> spelling

  let value_order f = function
    | Keyword k -> k.order
    | Themed t -> t.order
    | Spacing n -> f.spacing_off + spacing_value_order n
    | Fraction s -> fraction_value_order s
    | Arbitrary _ | Arbitrary_raw _ -> arbitrary_off
    (* A project-named size has no slot in the built-in scale, so it sorts with
       the other project-supplied values and ties are broken by class name. *)
    | Theme_named _ -> arbitrary_off

  let lookup f spelling =
    List.find_opt (fun v -> class_suffix v = spelling) f.entries

  (* [keyword prop name] is family [prop]'s entry for keyword [name]; every
     caller names one the family's table lists. *)
  let keyword prop name =
    let f = family prop in
    match lookup f name with
    | Some v -> Sized (prop, v)
    | None -> invalid_arg ("sizing: no such utility: " ^ f.prefix ^ "-" ^ name)

  let aspect_auto' = style [ Css.aspect_ratio Auto ]

  (* Theme variables for aspect ratios *)
  (* Tailwind's [@theme] declares [--aspect-video] after the perspective scale
     (8, 10-16) and before the [--default-*] tokens (8, 30+). *)
  let aspect_video_var = Var.theme Css.Aspect_ratio "aspect-video" ~order:(8, 20)

  (* aspect-square inlines the 1/1 ratio in v4 (no --aspect-square theme token),
     unlike aspect-video which references the --aspect-video token. *)
  let aspect_square' = style [ Css.aspect_ratio (Ratio (1., 1.)) ]
  let aspect_video_ratio : Css.aspect_ratio = Ratio (16., 9.)

  (* Published the same way, so [aspect-[theme(--aspect-video)]] resolves. *)
  let () = Theme.register_default aspect_video_var aspect_video_ratio

  let aspect_video' =
    let decl, r = Var.binding aspect_video_var aspect_video_ratio in
    style (decl :: [ Css.aspect_ratio (Var r) ])

  let aspect_ratio' w h = style [ Css.aspect_ratio (Ratio (w, h)) ]

  (* A [--aspect-*] token the project declared names a ratio the built-in scale
     has no slot for; the utility references it and the theme layer carries the
     project's own spelling. *)
  let aspect_named_cache = Domain_cache.v 8

  let aspect_named_var name =
    Domain_cache.or_add aspect_named_cache name (fun () ->
        Var.theme Css.Aspect_ratio ("aspect-" ^ name) ~order:(8, 21))

  let is_theme_aspect theme n =
    Scheme.theme_value (Some theme) ("aspect-" ^ n) <> None

  let parse_theme_ratio raw =
    match String.split_on_char '/' raw with
    | [ w; h ] -> (
        match
          ( float_of_string_opt (String.trim w),
            float_of_string_opt (String.trim h) )
        with
        | Some w, Some h -> Some (Css.Ratio (w, h) : Css.aspect_ratio)
        | _ -> None)
    | [ n ] ->
        Option.map
          (fun f : Css.aspect_ratio -> Css.Ratio (f, 1.))
          (float_of_string_opt (String.trim n))
    | _ -> None

  let aspect_theme' theme name =
    match Scheme.theme_value (Some theme) ("aspect-" ^ name) with
    | None -> style []
    | Some raw -> (
        match parse_theme_ratio raw with
        | None -> style []
        | Some ratio ->
            let decl, r = Var.binding (aspect_named_var name) ratio in
            style [ decl; Css.aspect_ratio (Var r) ])

  (* v4 resolves a named size through the namespaces its family lists before the
     container scale: [w-sm] reads [--width-sm], then [--spacing-sm], and only
     then the [--container-sm] default. *)
  let named_namespaces = function
    | Width -> [ "width"; "spacing" ]
    | Min_width -> [ "min-width"; "spacing" ]
    | Max_width -> [ "max-width"; "spacing"; "container" ]
    | Inline_size | Min_inline_size | Max_inline_size -> [ "spacing" ]
    | _ -> []

  let named_override theme prop spelling : Style.t option =
    let f = family prop in
    List.find_map
      (fun namespace ->
        let token = namespace ^ "-" ^ spelling in
        Option.map
          (fun value ->
            let decl =
              Css.custom_property ~layer:"theme" ("--" ^ token) value
            in
            let length : length = Var (Var.theme_ref token) in
            style (decl :: List.map (fun decl -> decl length) f.decls))
          (Scheme.theme_value (Some theme) token))
      (named_namespaces prop)

  let is_theme_named theme prop spelling =
    List.exists
      (fun namespace ->
        Scheme.theme_value (Some theme) (namespace ^ "-" ^ spelling) <> None)
      (named_namespaces prop)

  let sized_style theme prop v =
    let f = family prop in
    let set len = style (List.map (fun decl -> decl len) f.decls) in
    let set_arbitrary raw len =
      let theme_decl : Css.declaration option =
        let bare_name = Parse.extract_var_name raw in
        if String.equal bare_name raw then Option.None
        else
          Scheme.token theme bare_name
          |> Option.map (fun value ->
              Css.custom_property ~layer:"theme" ("--" ^ bare_name) value)
      in
      style (Option.to_list theme_decl @ List.map (fun decl -> decl len) f.decls)
    in
    match v with
    | Keyword k -> set k.length
    | Arbitrary (raw, len) -> set_arbitrary raw len
    | Arbitrary_raw (_, value) ->
        style
          (List.filter_map
             (fun property -> Parse.opaque_declaration property value)
             f.properties)
    | Fraction s -> (
        match (Parse.fraction s, fraction_pct s) with
        | Some (n, m), Some _ -> set (Parse.fraction_length n m)
        | _ -> failwith ("Unknown " ^ f.css_name ^ " fraction: " ^ s))
    | Spacing n ->
        (* [n] is in rem; the class number is [n * 4] because --spacing is
           0.25rem. calc(var(--spacing) * n) keeps v4 compatibility. *)
        let decl, len = Theme.spacing_calc_float ~theme (n *. 4.) in
        style (decl :: List.map (fun d -> d len) f.decls)
    | Themed t -> (
        match named_override theme prop t.spelling with
        | Some s -> s
        | None -> style (t.decl :: List.map (fun d -> d t.value) f.decls))
    | Theme_named spelling -> (
        match named_override theme prop spelling with
        | Some s -> s
        | None -> style [])

  let to_style theme = function
    | Sized (prop, v) -> sized_style theme prop v
    | Aspect_auto -> aspect_auto'
    | Aspect_square -> aspect_square'
    | Aspect_video -> aspect_video'
    | Aspect_ratio (w, h) -> aspect_ratio' w h
    | Aspect_arbitrary (_, value) ->
        style (Option.to_list (Parse.opaque_declaration "aspect-ratio" value))
    | Aspect_theme name -> aspect_theme' theme name

  let err_not_utility = Error (`Msg "Not a sizing utility")

  let err_invalid_value name value =
    Error (`Msg ("Invalid " ^ name ^ " value: " ^ value))

  (* A [/] inside a bracket belongs to the value ([w-[calc(2px/2)]]), not to a
     fraction. *)
  let is_bracket v = String.starts_with ~prefix:"[" v

  let parse_arbitrary s : (string * Css.length) option =
    (* Parse bracket values: [4px], [1rem], [calc(100vh-4rem)], etc. Uses
       Css.parse_length for full CSS length parsing including calc(). Returns
       (raw_inner, parsed_length) where raw_inner is used for the CSS class name
       selector (preserving original formatting). *)
    if Parse.is_bracket_value s then
      let inner = Parse.bracket_inner s in
      (* A data-type hint chooses the longhand and says nothing about the value.
         A sizing family writes one longhand, so every hint lands here and the
         length reader is handed what follows it; [inner] keeps the hint because
         the class name has to. *)
      match Parse.value_after_hint inner with
      | None -> None
      | Some value ->
          Option.map (fun l -> (inner, l)) (Parse.arbitrary_length value)
    else None

  (* A spacing step, and a ratio part, is a non-negative multiple of 0.25
     ([isValidSpacingMultiplier]), so [w-1.75] and [8.5/11] are utilities while
     [w-1.7] and [1.23/4.56] are not. *)
  let is_quarter_multiple f = f >= 0. && Float.rem f 0.25 = 0.

  (* One parser for all thirteen sizing families: the family's own keyword
     table, then the fraction, bracket and spacing tails they share. *)
  let parse_sized ~theme prop v =
    let f = family prop in
    match lookup f v with
    | Some value -> Ok (Sized (prop, value))
    | None -> (
        if String.contains v '/' && not (is_bracket v) then
          if fraction_pct v <> None then Ok (Sized (prop, Fraction v))
          else err_invalid_value (f.css_name ^ " fraction") v
        else if is_bracket v then
          match parse_arbitrary v with
          | Some (raw, len) -> Ok (Sized (prop, Arbitrary (raw, len)))
          | None when not (Parse.is_bracket_value v) ->
              (* [w-[10px][20px]] is two brackets glued together, which is one
                 value to neither reader nor to Tailwind. *)
              err_invalid_value f.css_name v
          | None -> (
              (* The family names its longhands, so a bracket no length reader
                 took still reaches them. *)
              let inner = Parse.bracket_inner v in
              match Parse.arbitrary_declaration_value inner with
              | Some raw -> Ok (Sized (prop, Arbitrary_raw (inner, raw)))
              | None -> err_invalid_value f.css_name v)
        else
          match Parse.decimal_float v with
          | Some n when is_quarter_multiple n && Theme.has_spacing_step ~theme n
            ->
              Ok (Sized (prop, Spacing (n *. 0.25)))
          | _ ->
              (* A size the project named in its own [@theme], under one of the
                 namespaces this family reads. *)
              if is_theme_named theme prop v then
                Ok (Sized (prop, Theme_named v))
              else err_invalid_value f.css_name v)

  let parse_max_w_screen s =
    match lookup max_width_family ("screen-" ^ s) with
    | Some value -> Ok (Sized (Max_width, value))
    | None -> err_invalid_value "max-width screen size" s

  (* A bare [aspect-4/3] is a class suffix, so each part is a plain decimal and
     a quarter multiple, the way a spacing step is. The bracket spelling shares
     none of that: it is a token stream, parsed below. *)
  let parse_aspect_ratio s =
    match String.split_on_char '/' s with
    | [ w; h ] -> (
        match (Parse.decimal_float w, Parse.decimal_float h) with
        | Some w, Some h when is_quarter_multiple w && is_quarter_multiple h ->
            Ok (Aspect_ratio (w, h))
        | _ -> err_not_utility)
    | _ -> err_not_utility

  let of_class theme class_name =
    let parse_sized prop v = parse_sized ~theme prop v in
    match Parse.split_class class_name with
    | [ "w"; value ] -> parse_sized Width value
    | [ "h"; value ] -> parse_sized Height value
    | [ "min"; "w"; value ] -> parse_sized Min_width value
    | [ "min"; "h"; value ] -> parse_sized Min_height value
    | [ "max"; "w"; value ] -> parse_sized Max_width value
    | [ "max"; "w"; "screen"; size ] -> parse_max_w_screen size
    | [ "max"; "h"; value ] -> parse_sized Max_height value
    | [ "size"; value ] -> parse_sized Width_and_height value
    | [ "inline"; value ] -> parse_sized Inline_size value
    | [ "min"; "inline"; value ] -> parse_sized Min_inline_size value
    | [ "max"; "inline"; value ] -> parse_sized Max_inline_size value
    | [ "block"; value ] -> parse_sized Block_size value
    | [ "min"; "block"; value ] -> parse_sized Min_block_size value
    | [ "max"; "block"; value ] -> parse_sized Max_block_size value
    | [ "aspect"; "auto" ] -> Ok Aspect_auto
    | [ "aspect"; "square" ] -> Ok Aspect_square
    | [ "aspect"; "video" ] -> Ok Aspect_video
    (* Tailwind reads nothing inside an [aspect-[...]] bracket: it is one
       token-stream site, so [aspect-[foo]] and [aspect-[1.23/4.56]] both reach
       [aspect-ratio] as written. Only text that could end the declaration or
       swallow the rest of the rule names no utility. *)
    | [ "aspect"; value ] when Parse.is_bracket_value value -> (
        let inner = Parse.bracket_inner value in
        match Parse.arbitrary_declaration_value inner with
        | Some declared -> Ok (Aspect_arbitrary (inner, declared))
        | None -> err_not_utility)
    | [ "aspect"; value ] when is_theme_aspect theme value ->
        Ok (Aspect_theme value)
    | [ "aspect"; value ] -> parse_aspect_ratio value
    | _ -> err_not_utility

  let suborder = function
    | Sized (prop, v) ->
        let f = family prop in
        f.base + value_order f v
    (* Every aspect candidate writes the same property. One slot lets the
       natural class-name tie-break order numeric ratios, then brackets and
       keywords without large numerators spilling past the tail. *)
    | Aspect_ratio _ | Aspect_arbitrary _ | Aspect_auto | Aspect_square
    | Aspect_video | Aspect_theme _ ->
        aspect_base

  (** Priority 6: sizing utilities (w-*, h-*, max-w-*, ...) come before
      flex-1/flex-col in Tailwind's order. The logical ones are registered after
      every other utility, so they sort last instead of beside w-* and h-*. *)
  let priority u = if suborder u >= logical then logical_priority else 6

  let to_class = function
    | Sized (prop, v) -> (family prop).prefix ^ "-" ^ class_suffix v
    | Aspect_auto -> "aspect-auto"
    | Aspect_square -> "aspect-square"
    | Aspect_video -> "aspect-video"
    | Aspect_theme name -> "aspect-" ^ name
    | Aspect_ratio (w, h) -> "aspect-" ^ class_float w ^ "/" ^ class_float h
    | Aspect_arbitrary (raw, _) -> "aspect-[" ^ raw ^ "]"

  let examples =
    [
      keyword Height "auto";
      keyword Max_height "none";
      keyword Min_height "auto";
      keyword Width "auto";
      keyword Max_width "none";
      keyword Min_width "auto";
      Aspect_auto;
      keyword Block_size "auto";
      keyword Max_block_size "none";
      keyword Min_block_size "auto";
      keyword Inline_size "auto";
      keyword Max_inline_size "none";
      keyword Min_inline_size "auto";
    ]
end

open Handler

module Utility_factory = Utility.Make (Handler)
(** Register the sizing utility handlers *)

(** Public API returning Utility.t *)
let utility = Utility_factory.v

let () = () (* Ensure utility is defined before usage below *)

(* [n] is in rem units; the spacing scale steps by 0.25rem. *)
let spacing prop n = Sized (prop, Spacing n)
let fraction prop f = Sized (prop, Fraction f)
let scaled prop n = utility (spacing prop (float_of_int n *. 0.25))

(* The half-step sibling of [scaled]: same spacing-scale value, a fractional
   class number (e.g. [0.5]) instead of an integer one. *)
let scaled' prop n = utility (spacing prop (n *. 0.25))

(* Top-level wrappers returning Utility.t, following the Utility.Handler
   pattern *)

(* Width *)
let w n = scaled Width n
let w' n = scaled' Width n
let w_auto = utility (keyword Width "auto")
let w_full = utility (keyword Width "full")
let w_screen = utility (keyword Width "screen")
let w_min = utility (keyword Width "min")
let w_max = utility (keyword Width "max")
let w_fit = utility (keyword Width "fit")
let w_1_2 = utility (fraction Width "1/2")
let w_1_3 = utility (fraction Width "1/3")
let w_2_3 = utility (fraction Width "2/3")
let w_1_4 = utility (fraction Width "1/4")
let w_3_4 = utility (fraction Width "3/4")
let w_1_5 = utility (fraction Width "1/5")
let w_2_5 = utility (fraction Width "2/5")
let w_3_5 = utility (fraction Width "3/5")
let w_4_5 = utility (fraction Width "4/5")

(* Height *)
let h n = scaled Height n
let h' n = scaled' Height n
let h_auto = utility (keyword Height "auto")
let h_full = utility (keyword Height "full")
let h_screen = utility (keyword Height "screen")
let h_min = utility (keyword Height "min")
let h_max = utility (keyword Height "max")
let h_fit = utility (keyword Height "fit")
let h_1_2 = utility (fraction Height "1/2")
let h_1_3 = utility (fraction Height "1/3")
let h_2_3 = utility (fraction Height "2/3")
let h_1_4 = utility (fraction Height "1/4")
let h_3_4 = utility (fraction Height "3/4")
let h_1_5 = utility (fraction Height "1/5")
let h_2_5 = utility (fraction Height "2/5")
let h_3_5 = utility (fraction Height "3/5")
let h_4_5 = utility (fraction Height "4/5")

(* Min width *)
let min_w n = scaled Min_width n
let min_w' n = scaled' Min_width n
let min_w_0 = utility (keyword Min_width "0")
let min_w_full = utility (keyword Min_width "full")
let min_w_min = utility (keyword Min_width "min")
let min_w_max = utility (keyword Min_width "max")
let min_w_fit = utility (keyword Min_width "fit")

(* Max width *)
let max_w n = scaled Max_width n
let max_w' n = scaled' Max_width n
let max_w_none = utility (keyword Max_width "none")
let max_w_xs = utility (keyword Max_width "xs")
let max_w_sm = utility (keyword Max_width "sm")
let max_w_md = utility (keyword Max_width "md")
let max_w_lg = utility (keyword Max_width "lg")
let max_w_xl = utility (keyword Max_width "xl")
let max_w_2xl = utility (keyword Max_width "2xl")
let max_w_3xl = utility (keyword Max_width "3xl")
let max_w_4xl = utility (keyword Max_width "4xl")
let max_w_5xl = utility (keyword Max_width "5xl")
let max_w_6xl = utility (keyword Max_width "6xl")
let max_w_7xl = utility (keyword Max_width "7xl")
let max_w_full = utility (keyword Max_width "full")
let max_w_min = utility (keyword Max_width "min")
let max_w_max = utility (keyword Max_width "max")
let max_w_fit = utility (keyword Max_width "fit")
let max_w_prose = utility (keyword Max_width "prose")
let max_w_screen_sm = utility (keyword Max_width "screen-sm")
let max_w_screen_md = utility (keyword Max_width "screen-md")
let max_w_screen_lg = utility (keyword Max_width "screen-lg")
let max_w_screen_xl = utility (keyword Max_width "screen-xl")
let max_w_screen_2xl = utility (keyword Max_width "screen-2xl")

(* Min height *)
let min_h n = scaled Min_height n
let min_h' n = scaled' Min_height n
let min_h_0 = utility (keyword Min_height "0")
let min_h_full = utility (keyword Min_height "full")
let min_h_screen = utility (keyword Min_height "screen")
let min_h_min = utility (keyword Min_height "min")
let min_h_max = utility (keyword Min_height "max")
let min_h_fit = utility (keyword Min_height "fit")

(* Max height *)
let max_h n = scaled Max_height n
let max_h' n = scaled' Max_height n
let max_h_none = utility (keyword Max_height "none")
let max_h_full = utility (keyword Max_height "full")
let max_h_screen = utility (keyword Max_height "screen")
let max_h_min = utility (keyword Max_height "min")
let max_h_max = utility (keyword Max_height "max")
let max_h_fit = utility (keyword Max_height "fit")

(* Size (width and height combined) *)
let size n = scaled Width_and_height n
let size' n = scaled' Width_and_height n
let size_auto = utility (keyword Width_and_height "auto")
let size_full = utility (keyword Width_and_height "full")
let size_min = utility (keyword Width_and_height "min")
let size_max = utility (keyword Width_and_height "max")
let size_fit = utility (keyword Width_and_height "fit")

(* Aspect ratio *)
let aspect_auto = utility Aspect_auto
let aspect_square = utility Aspect_square
let aspect_video = utility Aspect_video
let aspect_ratio w h = utility (Aspect_ratio (float_of_int w, float_of_int h))

(* Order exposure for this module *)
let order (u : Utility.base) =
  if String.equal (Utility.name_of_base u) Handler.name then
    Some (Utility.order u)
  else None

(* Export container theme variables for use by other modules (e.g., Columns) *)
let container_binding = Handler.container_binding
let container_3xs = Handler.container_3xs
let container_2xs = Handler.container_2xs
let container_xs = Handler.container_xs
let container_sm = Handler.container_sm
let container_md = Handler.container_md
let container_lg = Handler.container_lg
let container_xl = Handler.container_xl
let container_2xl = Handler.container_2xl
let container_3xl = Handler.container_3xl
let container_4xl = Handler.container_4xl
let container_5xl = Handler.container_5xl
let container_6xl = Handler.container_6xl
let container_7xl = Handler.container_7xl