@fungi.computer/caps 0.4.0 → 0.6.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/dist/styles.css CHANGED
@@ -4304,6 +4304,9 @@
4304
4304
  .end {
4305
4305
  inset-inline-end: var(--spacing);
4306
4306
  }
4307
+ .top-\(--resize-pane\) {
4308
+ top: var(--resize-pane);
4309
+ }
4307
4310
  .top-0 {
4308
4311
  top: calc(var(--spacing) * 0);
4309
4312
  }
@@ -4319,6 +4322,9 @@
4319
4322
  .bottom-4 {
4320
4323
  bottom: calc(var(--spacing) * 4);
4321
4324
  }
4325
+ .left-\(--resize-pane\) {
4326
+ left: var(--resize-pane);
4327
+ }
4322
4328
  .left-0 {
4323
4329
  left: calc(var(--spacing) * 0);
4324
4330
  }
@@ -11493,29 +11499,23 @@
11493
11499
  }
11494
11500
  .caps-resize-handle {
11495
11501
  display: grid;
11496
- width: 64px;
11497
- height: 22px;
11502
+ width: 44px;
11503
+ height: 24px;
11504
+ grid-template-columns: minmax(0, 1fr);
11498
11505
  place-items: center;
11499
- border: var(--border) solid var(--stipe-linework);
11506
+ border: 0;
11500
11507
  border-radius: var(--radius-field);
11501
- background: var(--color-base-200);
11508
+ background: transparent;
11502
11509
  cursor: row-resize;
11503
11510
  touch-action: none;
11504
11511
  user-select: none;
11505
11512
  }
11506
11513
  .caps-resize-handle[aria-orientation="vertical"] {
11507
- width: 22px;
11508
- height: 64px;
11514
+ width: 24px;
11515
+ height: 44px;
11509
11516
  cursor: col-resize;
11510
11517
  }
11511
- .caps-resize-handle[data-resize-variant="notch"] {
11512
- border-radius: var(--radius-box) var(--radius-box) var(--radius-field) var(--radius-field);
11513
- box-shadow: inset 0 1px 0 oklch(100% 0 0 / calc(var(--depth) * 0.55)), 0 calc(var(--depth) * 1px) calc(var(--depth) * 2px) var(--color-base-content);
11514
- @supports (color: color-mix(in lab, red, red)) {
11515
- box-shadow: inset 0 1px 0 oklch(100% 0 0 / calc(var(--depth) * 0.55)), 0 calc(var(--depth) * 1px) calc(var(--depth) * 2px) color-mix( in oklab, var(--color-base-content) calc(var(--depth) * 12%), transparent );
11516
- }
11517
- }
11518
- .caps-resize-handle:is(:hover, :focus-visible) {
11518
+ .caps-resize-handle:focus-visible {
11519
11519
  outline: 2px solid var(--color-info);
11520
11520
  outline-offset: 2px;
11521
11521
  }
@@ -11524,29 +11524,45 @@
11524
11524
  }
11525
11525
  .caps-resize-grip {
11526
11526
  position: relative;
11527
- width: 38px;
11528
- height: 8px;
11527
+ width: 28px;
11528
+ height: 6px;
11529
11529
  border-radius: var(--radius-button);
11530
- background-color: var(--color-neutral);
11531
- background-image: linear-gradient( to bottom, oklch(100% 0 0 / calc(var(--depth) * 0.08)), transparent 50%, oklch(0% 0 0 / calc(var(--depth) * 0.015)) );
11532
- box-shadow: var( --stipe-shadow-raised, inset 0 1px 0 oklch(100% 0 0 / calc(var(--depth) * 0.6)), inset 0 -2px 0 var(--color-neutral), inset 0 0 0 1px var(--color-neutral) );
11530
+ background-color: var(--color-base-200);
11531
+ box-shadow: 0 0 0 1px var(--stipe-linework), inset 0 1px 0 oklch(100% 0 0 / calc(var(--depth) * 0.18)), 0 1px 0 var(--color-base-200);
11533
11532
  @supports (color: color-mix(in lab, red, red)) {
11534
- box-shadow: var( --stipe-shadow-raised, inset 0 1px 0 oklch(100% 0 0 / calc(var(--depth) * 0.6)), inset 0 -2px 0 color-mix(in oklab, var(--color-neutral) 75%, black), inset 0 0 0 1px color-mix( in oklab, var(--color-neutral), black calc(var(--depth) * 14%) ) );
11533
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--stipe-linework) 80%, var(--color-base-200)), inset 0 1px 0 oklch(100% 0 0 / calc(var(--depth) * 0.18)), 0 1px 0 color-mix( in oklab, var(--color-base-200), var(--color-base-content) 14% );
11535
11534
  }
11536
11535
  }
11536
+ .caps-resize-handle:hover .caps-resize-grip {
11537
+ background-color: var(--color-base-300);
11538
+ }
11537
11539
  .caps-resize-grip::after {
11538
11540
  content: "";
11539
11541
  position: absolute;
11540
- inset-inline: 7px;
11542
+ left: 50%;
11541
11543
  top: 50%;
11544
+ width: 2px;
11542
11545
  height: 2px;
11543
- transform: translateY(-50%);
11544
- border-radius: var(--radius-button);
11545
- background: linear-gradient( to bottom, oklch(100% 0 0 / calc(var(--depth) * 0.45)) 50%, oklch(0% 0 0 / calc(var(--depth) * 0.28)) 50% );
11546
- box-shadow: 0 -3px 0 -0.5px oklch(100% 0 0 / calc(var(--depth) * 0.45)), 0 -2px 0 -0.5px oklch(0% 0 0 / calc(var(--depth) * 0.28)), 0 3px 0 -0.5px oklch(100% 0 0 / calc(var(--depth) * 0.45)), 0 4px 0 -0.5px oklch(0% 0 0 / calc(var(--depth) * 0.28));
11546
+ transform: translate(-50%, -50%);
11547
+ border-radius: var(--radius-selector);
11548
+ background: var(--color-base-content);
11549
+ @supports (color: color-mix(in lab, red, red)) {
11550
+ background: color-mix(in oklab, var(--color-base-content) 45%, transparent);
11551
+ }
11552
+ box-shadow: -5px 0 0 var(--color-base-content), 5px 0 0 var(--color-base-content);
11553
+ @supports (color: color-mix(in lab, red, red)) {
11554
+ box-shadow: -5px 0 0 color-mix(in oklab, var(--color-base-content) 45%, transparent), 5px 0 0 color-mix(in oklab, var(--color-base-content) 45%, transparent);
11555
+ }
11547
11556
  }
11548
11557
  .caps-resize-handle[aria-orientation="vertical"] .caps-resize-grip {
11549
- transform: rotate(90deg);
11558
+ width: 6px;
11559
+ height: 28px;
11560
+ }
11561
+ .caps-resize-handle[aria-orientation="vertical"] .caps-resize-grip::after {
11562
+ box-shadow: 0 -5px 0 var(--color-base-content), 0 5px 0 var(--color-base-content);
11563
+ @supports (color: color-mix(in lab, red, red)) {
11564
+ box-shadow: 0 -5px 0 color-mix(in oklab, var(--color-base-content) 45%, transparent), 0 5px 0 color-mix(in oklab, var(--color-base-content) 45%, transparent);
11565
+ }
11550
11566
  }
11551
11567
  @media (prefers-reduced-motion: reduce) {
11552
11568
  .caps-scroll-thumb-surface {
@@ -11554,6 +11570,10 @@
11554
11570
  }
11555
11571
  }
11556
11572
  @media (forced-colors: active) {
11573
+ .caps-resize-grip {
11574
+ border: 1px solid ButtonText;
11575
+ box-shadow: none;
11576
+ }
11557
11577
  .caps-scroll-track {
11558
11578
  background: Canvas;
11559
11579
  box-shadow: none;
@@ -13035,6 +13055,136 @@
13035
13055
  .caps-form-footer .caps-field-error {
13036
13056
  padding-bottom: var(--spacing-sm);
13037
13057
  }
13058
+ :is(.caps-form, .caps-form-section, .caps-fieldset)[data-layout="columns"] {
13059
+ --caps-form-label-col: 9rem;
13060
+ }
13061
+ .caps-form[data-layout="columns"] :is(.caps-form-body, .caps-form-section-fields) {
13062
+ gap: var(--spacing-sm);
13063
+ }
13064
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) {
13065
+ display: grid;
13066
+ grid-template-columns: var(--caps-form-label-col) minmax(0, 1fr);
13067
+ column-gap: var(--spacing-md);
13068
+ row-gap: var(--spacing-2xs);
13069
+ align-items: center;
13070
+ justify-content: stretch;
13071
+ --caps-field-control-width: 100%;
13072
+ }
13073
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-field-copy {
13074
+ display: contents;
13075
+ }
13076
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-field-copy > label {
13077
+ grid-column: 1;
13078
+ grid-row: 1;
13079
+ min-width: 0;
13080
+ overflow: hidden;
13081
+ text-overflow: ellipsis;
13082
+ white-space: nowrap;
13083
+ }
13084
+ .caps-fieldset[data-layout="columns"] > .caps-fieldset-legend {
13085
+ min-width: 0;
13086
+ overflow: hidden;
13087
+ text-overflow: ellipsis;
13088
+ white-space: nowrap;
13089
+ }
13090
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-field-copy > .caps-field-description {
13091
+ grid-column: 2;
13092
+ grid-row: 2;
13093
+ }
13094
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > :not(.caps-field-copy, .caps-field-lead, .caps-field-error-reveal) {
13095
+ grid-column: 2;
13096
+ grid-row: 1;
13097
+ width: 100%;
13098
+ max-width: 30rem;
13099
+ }
13100
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-switch {
13101
+ width: 40px;
13102
+ justify-self: start;
13103
+ }
13104
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-checkbox {
13105
+ width: 24px;
13106
+ justify-self: start;
13107
+ }
13108
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field[data-orientation="vertical"] > .caps-field-lead {
13109
+ grid-column: 1;
13110
+ grid-row: 1;
13111
+ margin: 0;
13112
+ }
13113
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field[data-orientation="vertical"] > .caps-field-error-reveal {
13114
+ grid-column: 2;
13115
+ grid-row: 2;
13116
+ }
13117
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field[data-orientation="horizontal"] > .caps-field-row > .caps-field-lead {
13118
+ grid-column: 1;
13119
+ grid-row: 1;
13120
+ }
13121
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field[data-orientation="horizontal"] > .caps-field-error-reveal {
13122
+ padding-inline-start: calc(var(--caps-form-label-col) + var(--spacing-md));
13123
+ }
13124
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field:has(> .caps-field-error-reveal[data-open="true"]) .caps-field-description, .caps-fieldset[data-layout="columns"]:has( > .caps-field-error-reveal[data-open="true"] ) > .caps-fieldset-description {
13125
+ display: none;
13126
+ }
13127
+ .caps-fieldset[data-layout="columns"] {
13128
+ display: grid;
13129
+ grid-template-columns: var(--caps-form-label-col) minmax(0, 1fr);
13130
+ column-gap: var(--spacing-md);
13131
+ row-gap: var(--spacing-2xs);
13132
+ align-items: start;
13133
+ }
13134
+ .caps-fieldset[data-layout="columns"] > .caps-fieldset-legend {
13135
+ grid-column: 1;
13136
+ grid-row: 1;
13137
+ margin: 0;
13138
+ }
13139
+ .caps-fieldset[data-layout="columns"] > .caps-fieldset-fields {
13140
+ grid-column: 2;
13141
+ grid-row: 1;
13142
+ flex-direction: row;
13143
+ flex-wrap: wrap;
13144
+ margin: 0;
13145
+ }
13146
+ .caps-fieldset[data-layout="columns"] > :is(.caps-fieldset-description, .caps-field-error-reveal) {
13147
+ grid-column: 2;
13148
+ grid-row: 2;
13149
+ }
13150
+ .caps-form[data-layout="columns"] .caps-form-footer {
13151
+ padding-inline-start: calc(var(--caps-form-label-col) + var(--spacing-md));
13152
+ max-width: calc(var(--caps-form-label-col) + var(--spacing-md) + 30rem);
13153
+ }
13154
+ @container (min-width: 36rem) {
13155
+ .caps-form[data-layout="columns"] .caps-form-body:has(> .caps-field:first-child:nth-last-child(2)) {
13156
+ display: grid;
13157
+ grid-template-columns: minmax(0, 1fr) auto;
13158
+ align-items: start;
13159
+ }
13160
+ .caps-form[data-layout="columns"] .caps-form-body:has(> .caps-field:first-child:nth-last-child(2)) > .caps-form-footer {
13161
+ padding-inline-start: 0;
13162
+ max-width: none;
13163
+ }
13164
+ }
13165
+ @container (max-width: 20rem) {
13166
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ), .caps-fieldset[data-layout="columns"] {
13167
+ grid-template-columns: minmax(0, 1fr);
13168
+ }
13169
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > :not(.caps-field-copy, .caps-field-lead, .caps-field-error-reveal), .caps-fieldset[data-layout="columns"] > .caps-fieldset-fields {
13170
+ grid-column: 1;
13171
+ grid-row: 2;
13172
+ max-width: none;
13173
+ }
13174
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-field-copy > .caps-field-description, :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field[data-orientation="vertical"] > .caps-field-error-reveal, .caps-fieldset[data-layout="columns"] > :is(.caps-fieldset-description, .caps-field-error-reveal) {
13175
+ grid-column: 1;
13176
+ grid-row: 3;
13177
+ }
13178
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] :is( .caps-field[data-orientation="vertical"], .caps-field[data-orientation="horizontal"] > .caps-field-row ) > .caps-field-copy > label, .caps-fieldset[data-layout="columns"] > .caps-fieldset-legend {
13179
+ overflow: visible;
13180
+ text-overflow: clip;
13181
+ white-space: normal;
13182
+ }
13183
+ :is(.caps-form, .caps-form-section)[data-layout="columns"] .caps-field[data-orientation="horizontal"] > .caps-field-error-reveal, .caps-form[data-layout="columns"] .caps-form-footer {
13184
+ padding-inline-start: 0;
13185
+ max-width: none;
13186
+ }
13187
+ }
13038
13188
  .caps-choice-card {
13039
13189
  display: flex;
13040
13190
  flex: 1 1 var(--caps-choice-card-basis, 14rem);
@@ -33,11 +33,11 @@ Props accepted by ReleaseTag.
33
33
 
34
34
  #### Type Declaration
35
35
 
36
- | Name | Type | Description |
37
- | --------- | ------------------------------------------------ | --------------------------------------------------------------------------- |
38
- | `title` | readonly `string`[] | One or two short lines printed large on the tag, such as ["Fungi", "Beta"]. |
39
- | `label?` | `ReactNode` | A small accent strip under the title, such as "Update". |
40
- | `entries` | readonly [`ReleaseTagEntry`](#releasetagentry)[] | - |
36
+ | Name | Type | Description |
37
+ | ---------- | ------------------------------------------------ | --------------------------------------------------------------------------- |
38
+ | `title` | readonly `string`[] | One or two short lines printed large on the tag, such as ["Fungi", "Beta"]. |
39
+ | `label?` | `ReactNode` | A small accent strip under the title, such as "Update". |
40
+ | `entries?` | readonly [`ReleaseTagEntry`](#releasetagentry)[] | Names and versions ruled under the page; omit for a tag with none. |
41
41
 
42
42
  ## Functions
43
43
 
@@ -48,8 +48,8 @@ function ReleaseTag(__namedParameters): Element;
48
48
  ```
49
49
 
50
50
  A release badge: a torn-off calendar page with binder rings, a stacked title,
51
- one accent strip and a ruled list of names and versions below. Presentation
52
- only; the caller supplies every word.
51
+ one accent strip and an optional ruled list of names and versions below.
52
+ Presentation only; the caller supplies every word.
53
53
 
54
54
  #### Parameters
55
55
 
@@ -45,16 +45,17 @@ Controlled selection and structural slots for a row of footer trays.
45
45
 
46
46
  #### Type Declaration
47
47
 
48
- | Name | Type | Description |
49
- | ---------------- | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
- | `items` | readonly [`StatusTrayItem`](#statustrayitem)[] | Readouts and their tray contents in display order. |
51
- | `openId` | `string` \| `null` | Which tray is open. The host owns this state. |
52
- | `onOpenChange()` | (`id`) => `void` | Requests a selection change or dismissal. |
53
- | `leading?` | `ReactNode` | Leading readouts that never open a tray (connection state, etc.). |
54
- | `trailing?` | `ReactNode` | Trailing controls after the chips (Send, Expand, …). |
55
- | `placement?` | `"housing"` \| `"screen"` \| `"host"` | Structural seat. `housing` sits on the plastic below a screen; `screen` sits inside a Display as flat, screen-native text (the wiki's screen material rule); `host` means a host's own chrome draws the chips, so only the open tray renders here. The look of each seat is theme-owned. |
56
- | `align?` | `"start"` \| `"end"` | Which edge the tray aligns to. |
57
- | `hostInset?` | `number` | For `host` placement: where the host's trigger starts, in CSS pixels from this tray's inline start, so the open tray sits above it. |
48
+ | Name | Type | Description |
49
+ | ------------------ | ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
50
+ | `items` | readonly [`StatusTrayItem`](#statustrayitem)[] | Readouts and their tray contents in display order. |
51
+ | `openId` | `string` \| `null` | Which tray is open. The host owns this state. |
52
+ | `onOpenChange()` | (`id`) => `void` | Requests a selection change or dismissal. |
53
+ | `leading?` | `ReactNode` | Leading readouts that never open a tray (connection state, etc.). |
54
+ | `trailing?` | `ReactNode` | Trailing controls after the chips (Send, Expand, …). |
55
+ | `renderTrigger()?` | (`props`) => `ReactNode` | Compose a native button trigger, forwarding these attributes, children and ref. StatusTray still owns disclosure, dismissal and return focus. Omit to retain the default chip and its theme-owned finish. |
56
+ | `placement?` | `"housing"` \| `"screen"` \| `"host"` | Structural seat. `housing` sits on the plastic below a screen; `screen` sits inside a Display as flat, screen-native text (the wiki's screen material rule); `host` means a host's own chrome draws the chips, so only the open tray renders here. The look of each seat is theme-owned. |
57
+ | `align?` | `"start"` \| `"end"` | Which edge the tray aligns to. |
58
+ | `hostInset?` | `number` | For `host` placement: where the host's trigger starts, in CSS pixels from this tray's inline start, so the open tray sits above it. |
58
59
 
59
60
  ## Functions
60
61
 
@@ -62,10 +62,26 @@ Props accepted by Dialog.
62
62
 
63
63
  ---
64
64
 
65
+ ### DialogPopupProps
66
+
67
+ ```ts
68
+ type DialogPopupProps = BaseDialog.Popup.Props & object;
69
+ ```
70
+
71
+ Props accepted by DialogPopup.
72
+
73
+ #### Type Declaration
74
+
75
+ | Name | Type | Description |
76
+ | ------- | ----------------------- | ------------------------------------------------------------------------- |
77
+ | `size?` | `"default"` \| `"fill"` | Fill the portal's positioning context instead of using the default width. |
78
+
79
+ ---
80
+
65
81
  ### DialogContentProps
66
82
 
67
83
  ```ts
68
- type DialogContentProps = BaseDialog.Popup.Props &
84
+ type DialogContentProps = DialogPopupProps &
69
85
  Pick<BaseDialog.Portal.Props, "container" | "keepMounted">;
70
86
  ```
71
87
 
@@ -39,9 +39,10 @@ Props accepted by Form.
39
39
 
40
40
  #### Type Declaration
41
41
 
42
- | Name | Type | Description |
43
- | ------- | --------- | ------------------------------------------------------- |
44
- | `busy?` | `boolean` | Disables every control while a submission is in flight. |
42
+ | Name | Type | Description |
43
+ | --------- | ----------- | ----------------------------------------------------------------- |
44
+ | `busy?` | `boolean` | Disables every control while a submission is in flight. |
45
+ | `layout?` | `"columns"` | Opt-in label/control grid; the default field rhythm is unchanged. |
45
46
 
46
47
  ---
47
48
 
@@ -55,12 +56,13 @@ Props accepted by FormSection.
55
56
 
56
57
  #### Type Declaration
57
58
 
58
- | Name | Type | Description |
59
- | -------------- | ----------------- | -------------------------------------- |
60
- | `title` | `ReactNode` | - |
61
- | `description?` | `ReactNode` | - |
62
- | `level?` | `2` \| `3` \| `4` | Heading level inside the page outline. |
63
- | `children` | `ReactNode` | - |
59
+ | Name | Type | Description |
60
+ | -------------- | ----------------- | ---------------------------------------------------------- |
61
+ | `title` | `ReactNode` | - |
62
+ | `description?` | `ReactNode` | - |
63
+ | `layout?` | `"columns"` | Opt-in label/control grid when used without a Form parent. |
64
+ | `level?` | `2` \| `3` \| `4` | Heading level inside the page outline. |
65
+ | `children` | `ReactNode` | - |
64
66
 
65
67
  ---
66
68
 
@@ -77,7 +79,9 @@ Props accepted by FieldSet.
77
79
  | Name | Type | Description |
78
80
  | -------------- | ----------- | ----------------------------------------------------------------- |
79
81
  | `legend` | `ReactNode` | - |
82
+ | `legendTitle?` | `string` | Full legend on hover when a columns form clips it to one line. |
80
83
  | `description?` | `ReactNode` | - |
84
+ | `layout?` | `"columns"` | Opt-in label/control grid when used without a Form parent. |
81
85
  | `error?` | `ReactNode` | The group's announced error, such as "Choose at least one right". |
82
86
  | `children` | `ReactNode` | - |
83
87
 
@@ -100,6 +104,7 @@ Props accepted by Field.
100
104
  | `error?` | `ReactNode` | - |
101
105
  | `icon?` | `ReactNode` | A decorative marker, such as a Sigil or App icon, before the label. It is hidden from assistive tech; the label stays the control's name. |
102
106
  | `label?` | `ReactNode` | - |
107
+ | `labelTitle?` | `string` | Full label on hover when a columns form clips it to one line. |
103
108
  | `orientation?` | `"horizontal"` \| `"inline"` \| `"vertical"` | `vertical` stacks label, control and error; `horizontal` puts the control opposite its copy (settings rows); `inline` puts a checkbox or switch before its copy. |
104
109
  | `optional?` | `boolean` | Marks an optional field in a mostly required form. |
105
110
  | `readout?` | `boolean` | The child is a read-only readout, such as a Status, not a control: the label names the row and nothing is wired to the child. |
package/docs/api/form.md CHANGED
@@ -155,6 +155,14 @@ Props accepted by FormFields.
155
155
  model: SchemaFormModel<V>;
156
156
  ```
157
157
 
158
+ ##### layout?
159
+
160
+ ```ts
161
+ optional layout?: "columns";
162
+ ```
163
+
164
+ Opt-in label/control grid for generated sections and groups.
165
+
158
166
  ##### names?
159
167
 
160
168
  ```ts
@@ -1286,6 +1294,32 @@ Receives the parsed value, never the raw input.
1286
1294
  \| `Promise`\<[`SubmitResult`](#submitresult)\<`V`\> \| `undefined`\> \|
1287
1295
  `Promise`\<`void`\> \| [`SubmitResult`](#submitresult)\<`V`\> \| `undefined`
1288
1296
 
1297
+ ##### onSaved?
1298
+
1299
+ ```ts
1300
+ optional onSaved?: (values) => void;
1301
+ ```
1302
+
1303
+ Called only after a saved result, with the new raw-value baseline.
1304
+
1305
+ ###### Parameters
1306
+
1307
+ | Parameter | Type |
1308
+ | --------- | ---- |
1309
+ | `values` | `V` |
1310
+
1311
+ ###### Returns
1312
+
1313
+ `void`
1314
+
1315
+ ##### failure?
1316
+
1317
+ ```ts
1318
+ optional failure?: string;
1319
+ ```
1320
+
1321
+ Safe message for a thrown submit; the cause is logged, never shown.
1322
+
1289
1323
  ---
1290
1324
 
1291
1325
  ### Validator
@@ -1508,14 +1542,16 @@ How a SchemaForm presents its actions and state.
1508
1542
 
1509
1543
  #### Type Declaration
1510
1544
 
1511
- | Name | Type | Description |
1512
- | --------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------- |
1513
- | `submitLabel?` | `ReactNode` | The primary action's label; defaults to "Save changes". |
1514
- | `discardLabel?` | `ReactNode` | Show a Discard action while there are unsaved changes. |
1515
- | `allowClean?` | `boolean` | Keep the primary action available on a clean form (create forms). |
1516
- | `footer?` | `ReactNode` | Replaces the SaveBar, such as a footer with a retry key; `null` drops it. |
1517
- | `children?` | `ReactNode` | Replaces the generated field list, for custom layouts such as Screen cells: place fields with `<FormFields model names={[…]} />`. |
1518
- | `disabled?` | `boolean` | Disable every field, such as while an uncertain request is retried. |
1545
+ | Name | Type | Description |
1546
+ | --------------- | ------------ | --------------------------------------------------------------------------------------------------------------------------------- |
1547
+ | `submitLabel?` | `ReactNode` | The primary action's label; defaults to "Save changes". |
1548
+ | `discardLabel?` | `ReactNode` | Show a Discard action while there are unsaved changes. |
1549
+ | `onCancel()?` | () => `void` | A secondary exit action, shown even when clean; never submits or discards. |
1550
+ | `cancelLabel?` | `ReactNode` | - |
1551
+ | `allowClean?` | `boolean` | Keep the primary action available on a clean form (create forms). |
1552
+ | `footer?` | `ReactNode` | Replaces the SaveBar, such as a footer with a retry key; `null` drops it. |
1553
+ | `children?` | `ReactNode` | Replaces the generated field list, for custom layouts such as Screen cells: place fields with `<FormFields model names={[…]} />`. |
1554
+ | `disabled?` | `boolean` | Disable every field, such as while an uncertain request is retried. |
1519
1555
 
1520
1556
  ---
1521
1557
 
@@ -1525,6 +1561,8 @@ How a SchemaForm presents its actions and state.
1525
1561
  type SchemaFormProps<V, Out> = SchemaFormPresentation &
1526
1562
  | {
1527
1563
  model: SchemaFormModel<V>;
1564
+ onSaved?: never;
1565
+ failure?: never;
1528
1566
  }
1529
1567
  | SchemaFormOptions<V, Out>;
1530
1568
  ```
@@ -1585,6 +1623,8 @@ Props accepted by SaveBar.
1585
1623
  | `model` | [`SchemaFormModel`](#schemaformmodel)\<`V`\> | - |
1586
1624
  | `label` | `ReactNode` | The primary action, such as "Save changes" or "Create Team". |
1587
1625
  | `discardLabel?` | `ReactNode` | Show a Discard action while there are unsaved changes. |
1626
+ | `onCancel()?` | () => `void` | A secondary exit action that never submits or resets the form. |
1627
+ | `cancelLabel?` | `ReactNode` | - |
1588
1628
  | `allowClean?` | `boolean` | Keep the primary action available on a clean form (create forms that may submit their defaults). By default a clean form has nothing to save. |
1589
1629
  | `dirtyLabel?` | `ReactNode` | The quiet readout while there are unsaved changes. |
1590
1630
  | `buttonProps?` | `Omit`\<[`ButtonProps`](components/button.md#buttonprops), `"type"` \| `"disabled"` \| `"loading"` \| `"children"` \| `"onClick"` \| `"asChild"`\> | - |
@@ -13,17 +13,19 @@
13
13
  ### formatMoney()
14
14
 
15
15
  ```ts
16
- function formatMoney(amount): string;
16
+ function formatMoney(amount, options?): string;
17
17
  ```
18
18
 
19
- Format USD with cents, preserving sub-cent precision without displaying non-zero
20
- amounts as zero.
19
+ Format USD to cents, marking non-zero sub-cent amounts rather than rounding them
20
+ to zero. Callers needing the supported five-decimal precision opt in with
21
+ `precision: "exact"`.
21
22
 
22
23
  #### Parameters
23
24
 
24
- | Parameter | Type |
25
- | --------- | -------- |
26
- | `amount` | `number` |
25
+ | Parameter | Type |
26
+ | ---------- | ------------------------------------------- |
27
+ | `amount` | `number` |
28
+ | `options?` | `Readonly`\<\{ `precision`: `"exact"`; \}\> |
27
29
 
28
30
  #### Returns
29
31
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fungi.computer/caps",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "author": "Levi <levi@technomancy.dev>",
5
5
  "description": "Product-neutral React primitives and blocks for Fungi interfaces",
6
6
  "type": "module",
@@ -476,7 +476,7 @@
476
476
  "devDependencies": {
477
477
  "@fungi.computer/maosaico": "0.1.0",
478
478
  "@fungi.computer/sprite": "0.2.0",
479
- "@fungi.computer/sprite-zdog": "0.2.0",
479
+ "@fungi.computer/sprite-zdog": "0.3.0",
480
480
  "@ladle/react": "5.1.1",
481
481
  "@tailwindcss/cli": "4.2.1",
482
482
  "@types/react": "18.3.3",
@@ -493,7 +493,7 @@
493
493
  "typescript": "5.9.3",
494
494
  "vite": "8.2.2",
495
495
  "vitest": "4.1.11",
496
- "@fungi.computer/sigils": "0.2.0",
496
+ "@fungi.computer/sigils": "0.2.1",
497
497
  "postcss-value-parser": "4.2.0",
498
498
  "@tanstack/react-form": "1.33.2",
499
499
  "effect": "4.0.2",
@@ -588,7 +588,7 @@
588
588
  "format:check": "node ../../tooling/repository/scripts/format-package-check.mjs",
589
589
  "lint:anti-slop": "node ../../tooling/repository/scripts/lint-package-anti-slop.mjs",
590
590
  "build:workbench": "node scripts/workbench-build.mjs --prepared",
591
- "test:nightly": "node --import tsx ../../tooling/repository/scripts/workbench-proofs.mjs --output ../../.botanical/caps-workbench --origin-env CAPS_WORKBENCH_URL test/screen-theme.browser.mjs test/scroll-area.browser.mjs test/display-density.browser.mjs && node test/scroll-gutter.browser.mjs",
591
+ "test:nightly": "node --import tsx ../../tooling/repository/scripts/workbench-proofs.mjs --output ../../.botanical/caps-workbench --origin-env CAPS_WORKBENCH_URL test/screen-theme.browser.mjs test/scroll-area.browser.mjs test/display-density.browser.mjs test/resize-handle.browser.mjs test/dialog-fill.browser.mjs && node test/scroll-gutter.browser.mjs",
592
592
  "test:code-input": "node --import tsx ../../tooling/repository/scripts/workbench-proofs.mjs --output ../../.botanical/caps-code-input --origin-env CAPS_WORKBENCH_URL test/code-input.browser.mjs",
593
593
  "test:icon-choice": "node --import tsx ../../tooling/repository/scripts/workbench-proofs.mjs --output ../../.botanical/caps-icon-choice --origin-env CAPS_WORKBENCH_URL test/icon-choice.browser.mjs",
594
594
  "test:choice-label-alignment": "node --import tsx ../../tooling/repository/scripts/workbench-proofs.mjs --output ../../.botanical/caps-choice-alignment --origin-env CAPS_WORKBENCH_URL test/choice-label-alignment.browser.mjs",