@sigx/zero-daisyui 0.13.0 → 0.14.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/recipes.js CHANGED
@@ -3674,6 +3674,80 @@ export const avatar = {
3674
3674
  xl: { root: { base: { '--avatar-size': 'calc(var(--size-selector) * 16)', '--avatar-text': 'var(--text-lg)' } } },
3675
3675
  },
3676
3676
  },
3677
+ // signalxjs/lynx#1237 — the lynx layout for what the web gets from grid.
3678
+ targets: {
3679
+ lynx: {
3680
+ parts: {
3681
+ // No grid on lynx (signalxjs/lynx#1075): the web stacks the
3682
+ // image and the fallback in one grid cell. Here the root is a
3683
+ // flex box that does not shrink in a row, and both faces are
3684
+ // laid over it absolutely — the fallback, later in the tree,
3685
+ // paints over a pending image exactly as the grid's second
3686
+ // item does. lynx-zero renders neither face while its state
3687
+ // hides it (`hiddenIn`), so the web's `&:not([hidden])`
3688
+ // display rule has nothing to replace.
3689
+ root: {
3690
+ base: {
3691
+ display: 'flex',
3692
+ flexDirection: 'row',
3693
+ alignItems: 'center',
3694
+ justifyContent: 'center',
3695
+ flexShrink: '0',
3696
+ },
3697
+ },
3698
+ // Each face also rounds itself to the root's shape (below),
3699
+ // rather than trusting the root's clip to round an image on
3700
+ // both engines.
3701
+ image: {
3702
+ base: { position: 'absolute', top: '0', left: '0', borderRadius: '9999px' },
3703
+ },
3704
+ fallback: {
3705
+ base: {
3706
+ position: 'absolute',
3707
+ top: '0',
3708
+ left: '0',
3709
+ borderRadius: '9999px',
3710
+ display: 'flex',
3711
+ alignItems: 'center',
3712
+ justifyContent: 'center',
3713
+ textAlign: 'center',
3714
+ },
3715
+ },
3716
+ },
3717
+ variants: {
3718
+ shape: Object.fromEntries([
3719
+ ['circle', '9999px'], ['square', '0'], ['rounded', 'var(--radius-selector)'],
3720
+ ].map(([shape, radius]) => [shape, {
3721
+ image: { base: { borderRadius: radius } },
3722
+ fallback: { base: { borderRadius: radius } },
3723
+ }])),
3724
+ },
3725
+ // Every avatar after the first in an AvatarGroup overlaps its
3726
+ // neighbour by a quarter of its own box — the web's
3727
+ // `avatar-group` compose, `&:not(:first-child)`. Lynx has neither
3728
+ // the nested-scope compose nor `:first-child`, so lynx-zero stamps
3729
+ // those avatars `stacked` itself (`zx-m-stacked`, in mount order).
3730
+ // A lynx-zero rendering detail, not a modifier an author sets, so
3731
+ // it is styled from raw lynx css, one rule per avatar size.
3732
+ css: `
3733
+ .zx-avatar__root.zx-m-stacked {
3734
+ margin-left: calc(var(--size-selector) * -2.5);
3735
+ }
3736
+ .zx-avatar__root.zx-m-stacked.zx-a-size-xs {
3737
+ margin-left: calc(var(--size-selector) * -1.5);
3738
+ }
3739
+ .zx-avatar__root.zx-m-stacked.zx-a-size-sm {
3740
+ margin-left: calc(var(--size-selector) * -2);
3741
+ }
3742
+ .zx-avatar__root.zx-m-stacked.zx-a-size-lg {
3743
+ margin-left: calc(var(--size-selector) * -3);
3744
+ }
3745
+ .zx-avatar__root.zx-m-stacked.zx-a-size-xl {
3746
+ margin-left: calc(var(--size-selector) * -4);
3747
+ }
3748
+ `,
3749
+ },
3750
+ },
3677
3751
  };
3678
3752
  /**
3679
3753
  * Avatar group (#297) — daisy's `avatar-group`: the faces overlap by a
@@ -3766,6 +3840,33 @@ export const avatarGroup = {
3766
3840
  md: {},
3767
3841
  },
3768
3842
  },
3843
+ // signalxjs/lynx#1237. The avatars inside take the group's size and
3844
+ // colour from lynx-zero (the `composes` above has no class form on this
3845
+ // target), and their overlap is the avatar recipe's `stacked` rule.
3846
+ targets: {
3847
+ lynx: {
3848
+ parts: {
3849
+ // A row that sizes to its faces (a lynx view stretches to its
3850
+ // column otherwise).
3851
+ root: { base: { display: 'flex', flexDirection: 'row', width: 'max-content' } },
3852
+ // `inline-grid` centring as flex, and the logical inline
3853
+ // padding and overlap as physical spellings — logical
3854
+ // margins and paddings resolve on iOS but not on Android
3855
+ // (signalxjs/lynx#1084); lynx has no RTL flow, so start IS left.
3856
+ overflow: {
3857
+ base: {
3858
+ display: 'flex',
3859
+ alignItems: 'center',
3860
+ justifyContent: 'center',
3861
+ flexShrink: '0',
3862
+ paddingLeft: 'var(--space-2xs)',
3863
+ paddingRight: 'var(--space-2xs)',
3864
+ marginLeft: 'calc(var(--avatar-group-size) * -0.25)',
3865
+ },
3866
+ },
3867
+ },
3868
+ },
3869
+ },
3769
3870
  };
3770
3871
  /**
3771
3872
  * A toast whose indicator is rendered (#292): the grid grows a leading column
@@ -4059,7 +4160,99 @@ export const toast = {
4059
4160
  base: { position: 'absolute', top: 'var(--space-xs)', right: 'var(--space-xs)' },
4060
4161
  states: lynxFocus({ gap: 'var(--toast-bg)' }),
4061
4162
  },
4163
+ // The promise toast's mark (signalxjs/lynx#1196). No grid and
4164
+ // no `:has()`: the mark is laid over the card's leading
4165
+ // padding, at the root's own padding offsets (per size,
4166
+ // below), and lynx-zero stamps `marked` on every part of a
4167
+ // toast that shows it, so the text steps aside (the raw css
4168
+ // below) — the web's leading grid column.
4169
+ indicator: {
4170
+ base: {
4171
+ position: 'absolute',
4172
+ left: 'var(--space-lg)',
4173
+ top: 'var(--space-md)',
4174
+ width: 'var(--toast-mark)',
4175
+ height: 'var(--toast-mark)',
4176
+ },
4177
+ states: {
4178
+ // daisy's ring in the toast's ink. The track is the
4179
+ // ink at 25%, which cannot bake over the recipe-local
4180
+ // `--toast-ink`: the neutral toast's track mixes the
4181
+ // theme's base-content, and each colour's twin (the
4182
+ // variants below) its role. Longhands, and the head as
4183
+ // `border-top-color`: logical spellings are not lynx's
4184
+ // (signalxjs/lynx#1084), and a var-bearing shorthand
4185
+ // would repaint the head after the cascade (#1161).
4186
+ loading: {
4187
+ // The shared shorthand is dropped on lynx (its
4188
+ // track cannot bake); a static one here keeps it
4189
+ // that way should the shared value ever bake.
4190
+ border: '0 solid transparent',
4191
+ borderStyle: 'solid',
4192
+ ...lynxSides('border{}Width', 'calc(var(--border) * 2)'),
4193
+ ...lynxSides('border{}Color', 'color-mix(in oklab, var(--color-base-content) 25%, transparent)', ['Right', 'Bottom', 'Left']),
4194
+ borderTopColor: 'var(--toast-ink)',
4195
+ },
4196
+ // The tick without clip-path, which lynx does not apply
4197
+ // (signalxjs/lynx#1216): the checkbox's two-border
4198
+ // check — a box 3/8 of the mark wide and 3/4 tall with
4199
+ // its right and bottom borders drawn, turned 45° —
4200
+ // pulled to the middle of the mark's slot by its
4201
+ // margins. The fill the web cuts the tick from is
4202
+ // cleared.
4203
+ complete: {
4204
+ background: 'transparent',
4205
+ boxSizing: 'border-box',
4206
+ width: 'calc(var(--toast-mark) * 0.375)',
4207
+ height: 'calc(var(--toast-mark) * 0.75)',
4208
+ marginLeft: 'calc(var(--toast-mark) * 0.3125)',
4209
+ borderStyle: 'solid',
4210
+ borderColor: TOAST_SUCCESS_INK,
4211
+ borderTopWidth: '0',
4212
+ borderLeftWidth: '0',
4213
+ borderRightWidth: 'calc(var(--border) * 2)',
4214
+ borderBottomWidth: 'calc(var(--border) * 2)',
4215
+ transform: 'rotate(45deg)',
4216
+ },
4217
+ // The cross without clip-path: its two diagonal bars
4218
+ // as hard-stop gradients over a clear fill.
4219
+ error: {
4220
+ background: `linear-gradient(45deg, transparent 41%, ${TOAST_ERROR_INK} 41%, ${TOAST_ERROR_INK} 59%, transparent 59%), `
4221
+ + `linear-gradient(-45deg, transparent 41%, ${TOAST_ERROR_INK} 41%, ${TOAST_ERROR_INK} 59%, transparent 59%)`,
4222
+ },
4223
+ },
4224
+ },
4225
+ },
4226
+ variants: {
4227
+ // Each colour's ring track: its role at 25% (the web's mix
4228
+ // over `--toast-ink`, which is that role).
4229
+ color: Object.fromEntries(ROLES.map((role) => [role, {
4230
+ indicator: {
4231
+ states: {
4232
+ loading: lynxSides('border{}Color', `color-mix(in oklab, var(--color-${role}) 25%, transparent)`, ['Right', 'Bottom', 'Left']),
4233
+ },
4234
+ },
4235
+ }])),
4236
+ // The mark sits at the root's padding offsets for its size.
4237
+ size: {
4238
+ xs: { indicator: { base: { left: 'var(--space-md)', top: 'var(--space-xs)' } } },
4239
+ sm: { indicator: { base: { left: 'var(--space-md)', top: 'var(--space-sm)' } } },
4240
+ lg: { indicator: { base: { left: 'var(--space-xl)', top: 'var(--space-lg)' } } },
4241
+ xl: { indicator: { base: { left: 'var(--space-2xl)', top: 'var(--space-xl)' } } },
4242
+ },
4062
4243
  },
4244
+ // The text of a marked toast steps past the mark and the web's
4245
+ // column gap. `marked` is a lynx-zero rendering detail (the
4246
+ // spelling of `:has(> indicator)`), not a modifier an author sets,
4247
+ // so it is styled from raw lynx css. The reduced-motion and
4248
+ // forced-colors stops cannot follow: lynx emits no `@media`.
4249
+ css: `
4250
+ .zx-toast__title.zx-m-marked,
4251
+ .zx-toast__description.zx-m-marked,
4252
+ .zx-toast__action.zx-m-marked {
4253
+ margin-left: calc(var(--toast-mark) + var(--space-md));
4254
+ }
4255
+ `,
4063
4256
  },
4064
4257
  },
4065
4258
  // The viewport's `open` (the stack expanded) is a fact this skin has no
@@ -4154,7 +4347,8 @@ export const combobox = {
4154
4347
  fontWeight: 'var(--weight-medium)',
4155
4348
  lineHeight: 'var(--leading-none)',
4156
4349
  },
4157
- states: { disabled: {} },
4350
+ // Arrowed onto from the input (#411).
4351
+ states: { disabled: {}, ...focusRing },
4158
4352
  },
4159
4353
  'tag-label': { base: { whiteSpace: 'nowrap' } },
4160
4354
  'tag-remove': {
@@ -5510,11 +5704,18 @@ export const textarea = {
5510
5704
  // lands on top of the 2px gap and paints one solid band. A fixed-height
5511
5705
  // box never re-lays out; this auto-height one does, as its native field
5512
5706
  // measures and grows. So the ring is spelled with one outset shadow and
5513
- // one inset shadow, which paint disjoint areas and cannot swap: the ring
5707
+ // inset shadows, which paint areas the outset never reaches: the ring
5514
5708
  // just outside the box, the box's own border repainted as the base-100
5515
- // gap, and the hairline redrawn inside it by the inset spread, in the
5709
+ // gap, and the hairline redrawn inside it by the edge inset's spread, in the
5516
5710
  // edge colour (`--textarea-edge`, the error colour while invalid). The
5517
5711
  // gap is the border's width instead of the web's 2px.
5712
+ //
5713
+ // Android paints inset shadows OVER the border, where iOS paints them
5714
+ // under it (signalxjs/lynx#1230): the base-100 border vanished and the
5715
+ // whole 2px inset showed as a grey band against the ring. So the gap is
5716
+ // also drawn as a 1px base-100 inset stacked above the edge inset. On
5717
+ // Android that inset is the gap. On iOS the border already covers it,
5718
+ // whichever order the layers end up in, so iOS paints as before.
5518
5719
  targets: {
5519
5720
  lynx: {
5520
5721
  parts: {
@@ -5531,6 +5732,9 @@ export const textarea = {
5531
5732
  outline: 'none',
5532
5733
  borderColor: 'var(--color-base-100)',
5533
5734
  boxShadow: '0 0 0 2px var(--textarea-accent), '
5735
+ // The gap: the border's width in base-100,
5736
+ // for Android, which paints this over the border.
5737
+ + 'inset 0 0 0 1px var(--color-base-100), '
5534
5738
  // Twice the border (1px in every shipped
5535
5739
  // theme): the inner half shows as the hairline.
5536
5740
  + 'inset 0 0 0 2px var(--textarea-edge)',
@@ -5656,6 +5860,42 @@ export const card = {
5656
5860
  xl: { root: { base: { '--card-pad': 'var(--space-2xl)' } } },
5657
5861
  },
5658
5862
  },
5863
+ targets: {
5864
+ lynx: {
5865
+ parts: {
5866
+ // The root does not clip on lynx: a clipping layer takes its
5867
+ // own shadow with it on iOS, and the card's elevation is the
5868
+ // daisy look. The media band rounds its own corners instead
5869
+ // (below), which is all the clip was for.
5870
+ root: { base: { overflow: 'visible' } },
5871
+ // `inline-size` has no lynx spelling; the image selector and
5872
+ // the `:first-child` / `:last-child` corners are dropped (no
5873
+ // child or structural pseudo-classes). The band clips its
5874
+ // own content, so an `<image>` inside it takes the corners.
5875
+ media: { base: { width: '100%', overflow: 'hidden' } },
5876
+ // A lynx view is a column unless told otherwise.
5877
+ footer: { base: { flexDirection: 'row' } },
5878
+ },
5879
+ // The corners the media band shares with the card
5880
+ // (signalxjs/lynx#1235). Lynx has no `:first-child` or
5881
+ // `:last-child`, so lynx-zero stamps the band itself from its
5882
+ // mount order among the card's bands, as `data-mod-first` /
5883
+ // `data-mod-last` (the `zx-m-first` / `zx-m-last` classes). They
5884
+ // are a lynx-zero rendering detail, not modifiers an author sets,
5885
+ // so they are not declared in the vocabulary and are styled from
5886
+ // raw lynx css (toggle-group's join ends are the precedent).
5887
+ css: `
5888
+ .zx-card__media.zx-m-first {
5889
+ border-top-left-radius: var(--radius-box);
5890
+ border-top-right-radius: var(--radius-box);
5891
+ }
5892
+ .zx-card__media.zx-m-last {
5893
+ border-bottom-left-radius: var(--radius-box);
5894
+ border-bottom-right-radius: var(--radius-box);
5895
+ }
5896
+ `,
5897
+ },
5898
+ },
5659
5899
  };
5660
5900
  /**
5661
5901
  * daisy "alert" flavor: the role's soft tint edge to edge, its ink for the
@@ -5746,6 +5986,87 @@ export const alert = {
5746
5986
  xl: { root: { base: { padding: 'var(--space-xl) var(--space-2xl)' } } },
5747
5987
  },
5748
5988
  },
5989
+ targets: {
5990
+ // No grid on lynx (signalxjs/lynx#1075, #1235): the web's three
5991
+ // columns (the icon over both rows | the title over the description
5992
+ // | the close button) are rebuilt as a flex column with the icon and
5993
+ // the close button placed absolutely — the toast's close pin is the
5994
+ // precedent. The padding moves onto two properties so the icon and
5995
+ // the close button can follow it per size.
5996
+ lynx: {
5997
+ tokens: {
5998
+ '--alert-pad-y': 'var(--space-md)',
5999
+ '--alert-pad-x': 'var(--space-lg)',
6000
+ },
6001
+ parts: {
6002
+ root: {
6003
+ base: {
6004
+ display: 'flex',
6005
+ flexDirection: 'column',
6006
+ alignItems: 'stretch',
6007
+ gap: 'var(--space-sm)',
6008
+ position: 'relative',
6009
+ boxSizing: 'border-box',
6010
+ padding: 'var(--alert-pad-y) var(--alert-pad-x)',
6011
+ },
6012
+ },
6013
+ // The icon column: a --text-xl slot at the padding edge,
6014
+ // centred over the alert's height (the web's two-row span).
6015
+ icon: {
6016
+ base: {
6017
+ position: 'absolute',
6018
+ left: 'var(--alert-pad-x)',
6019
+ top: '0',
6020
+ bottom: '0',
6021
+ width: 'var(--text-xl)',
6022
+ display: 'flex',
6023
+ justifyContent: 'center',
6024
+ },
6025
+ },
6026
+ // Pinned top-right, its glyph on the padding edge. `inherit`
6027
+ // is spelled as the root's own ink.
6028
+ close: {
6029
+ base: {
6030
+ position: 'absolute',
6031
+ top: 'calc(var(--alert-pad-y) - var(--space-2xs))',
6032
+ right: 'calc(var(--alert-pad-x) - var(--space-2xs))',
6033
+ color: 'var(--color-base-content)',
6034
+ fontSize: 'var(--text-md)',
6035
+ },
6036
+ states: lynxFocus({ gap: 'var(--alert-tint)' }),
6037
+ },
6038
+ },
6039
+ variants: {
6040
+ size: {
6041
+ xs: { root: { base: { '--alert-pad-y': 'var(--space-2xs)', '--alert-pad-x': 'var(--space-sm)' } } },
6042
+ sm: { root: { base: { '--alert-pad-y': 'var(--space-xs)', '--alert-pad-x': 'var(--space-md)' } } },
6043
+ lg: { root: { base: { '--alert-pad-y': 'var(--space-lg)', '--alert-pad-x': 'var(--space-xl)' } } },
6044
+ xl: { root: { base: { '--alert-pad-y': 'var(--space-xl)', '--alert-pad-x': 'var(--space-2xl)' } } },
6045
+ },
6046
+ },
6047
+ // The title and description keep clear of the icon column and
6048
+ // the close button's corner. Lynx has no `:has()`, so lynx-zero
6049
+ // stamps what the alert renders onto both text parts, as
6050
+ // `data-mod-with-icon` / `data-mod-with-close` (the
6051
+ // `zx-m-with-icon` / `zx-m-with-close` classes) — a lynx-zero
6052
+ // rendering detail like toggle-group's join ends, styled from raw
6053
+ // lynx css.
6054
+ css: `
6055
+ .zx-alert__title.zx-m-with-icon {
6056
+ margin-left: calc(var(--text-xl) + var(--space-md));
6057
+ }
6058
+ .zx-alert__description.zx-m-with-icon {
6059
+ margin-left: calc(var(--text-xl) + var(--space-md));
6060
+ }
6061
+ .zx-alert__title.zx-m-with-close {
6062
+ margin-right: calc(var(--text-md) + var(--space-md));
6063
+ }
6064
+ .zx-alert__description.zx-m-with-close {
6065
+ margin-right: calc(var(--text-md) + var(--space-md));
6066
+ }
6067
+ `,
6068
+ },
6069
+ },
5749
6070
  };
5750
6071
  /**
5751
6072
  * EmptyState (zero#131) — daisy has no empty-state component; this is the
@@ -5827,6 +6148,25 @@ export const emptyState = {
5827
6148
  xl: { root: { base: { padding: 'calc(var(--space-2xl) * 2) var(--space-2xl)', gap: 'var(--space-lg)' } }, icon: { base: { fontSize: 'var(--text-2xl)' } }, title: { base: { fontSize: 'var(--text-2xl)' } }, description: { base: { fontSize: 'var(--text-md)' } } },
5828
6149
  },
5829
6150
  },
6151
+ // signalxjs/lynx#1236 — the lynx replacements for what the shared
6152
+ // sections cannot say there.
6153
+ targets: {
6154
+ lynx: {
6155
+ parts: {
6156
+ // Physical margins: Android resolves no logical margin
6157
+ // (signalxjs/lynx#1084).
6158
+ icon: { base: { marginBottom: 'var(--space-xs)' } },
6159
+ // Title and description are lynx `<text>`: a view's
6160
+ // `text-align` and `color` do not reach one unless the app
6161
+ // turned CSS inheritance on, so each states its own ink and
6162
+ // centring. The web's 36ch measure becomes the root's width:
6163
+ // `ch` is unmeasured on lynx.
6164
+ title: { base: { color: 'var(--color-base-content)', textAlign: 'center' } },
6165
+ description: { base: { textAlign: 'center', maxInlineSize: '100%' } },
6166
+ actions: { base: { flexDirection: 'row', marginTop: 'var(--space-sm)' } },
6167
+ },
6168
+ },
6169
+ },
5830
6170
  };
5831
6171
  /**
5832
6172
  * daisy "badge" flavor: a fully-rounded pill with a hairline, filled by the
@@ -5928,6 +6268,62 @@ export const badge = {
5928
6268
  }, keyframes: {
5929
6269
  'zero-daisy-badge-pulse': 'from { box-shadow: 0 0 0 0 color-mix(in oklch, var(--badge-dot) 45%, transparent); } to { box-shadow: 0 0 0 0.5em transparent; }',
5930
6270
  },
6271
+ /**
6272
+ * Lynx (signalxjs/lynx#1234). The dot's `currentColor` never resolves
6273
+ * there (lynx#1079), so the uncoloured dot and its ring take the pill's
6274
+ * ink token, the value `currentColor` names on the web. The running halo
6275
+ * and pulse mix the recipe-local `--badge-dot`, which cannot bake, so the
6276
+ * halo is its own token mixed from each role's `--color-*` (baked per
6277
+ * theme) and the pulse breathes opacity instead of the halo's spread.
6278
+ * `inline-size`/`block-size` do not resolve on lynx (lynx#1145): the dot
6279
+ * is sized physically. A lynx view has no inline formatting context, so
6280
+ * the pill is content-sized with an explicit row (lynx#1165), and its
6281
+ * hairline and the dot's ring are longhands (lynx#1161/#1162).
6282
+ */
6283
+ targets: {
6284
+ lynx: {
6285
+ tokens: {
6286
+ '--badge-dot': 'var(--badge-ink)',
6287
+ '--badge-dot-ring': 'var(--badge-ink)',
6288
+ '--badge-dot-halo': 'color-mix(in oklch, var(--color-base-content) 35%, transparent)',
6289
+ },
6290
+ parts: {
6291
+ root: {
6292
+ base: {
6293
+ ...lynxBtnFit,
6294
+ flexDirection: 'row',
6295
+ border: '0 solid transparent',
6296
+ ...lynxSides('border{}Width', 'var(--border)'),
6297
+ },
6298
+ },
6299
+ dot: {
6300
+ base: {
6301
+ display: 'flex',
6302
+ width: '0.5em',
6303
+ height: '0.5em',
6304
+ flexShrink: '0',
6305
+ border: '0 solid transparent',
6306
+ ...lynxSides('border{}Width', '0.1em'),
6307
+ ...lynxSides('border{}Color', 'var(--badge-dot-ring)'),
6308
+ },
6309
+ states: {
6310
+ running: {
6311
+ boxShadow: '0 0 0 0.2em var(--badge-dot-halo)',
6312
+ animation: 'zero-daisy-badge-pulse 1.6s ease-in-out infinite',
6313
+ },
6314
+ },
6315
+ },
6316
+ },
6317
+ variants: {
6318
+ color: Object.fromEntries(ROLES.map((c) => [c, {
6319
+ dot: { base: { '--badge-dot-halo': `color-mix(in oklch, var(--color-${c}) 35%, transparent)` } },
6320
+ }])),
6321
+ },
6322
+ keyframes: {
6323
+ 'zero-daisy-badge-pulse': 'from, to { opacity: 1; } 50% { opacity: 0.45; }',
6324
+ },
6325
+ },
6326
+ },
5931
6327
  };
5932
6328
  /** daisy "divider": the base-300 rule, thickening with the ramp. */
5933
6329
  export const divider = {
@@ -5997,6 +6393,11 @@ export const divider = {
5997
6393
  },
5998
6394
  targets: {
5999
6395
  lynx: {
6396
+ // `auto` behind a custom property, so the labelled root's size
6397
+ // is var-bearing too and can beat the var-bearing line thickness
6398
+ // below (lynx expands var() declarations after the cascade,
6399
+ // signalxjs/lynx#1161).
6400
+ tokens: { '--divider-fit': 'auto' },
6000
6401
  parts: {
6001
6402
  // Lynx drops the ::before/::after segments the rule is drawn
6002
6403
  // with above, which left the lynx divider with no line and,
@@ -6011,7 +6412,64 @@ export const divider = {
6011
6412
  '&[data-orientation="vertical"]': { inlineSize: 'var(--divider-thickness)' },
6012
6413
  },
6013
6414
  },
6415
+ // The label's inset, physical (Android resolves no logical
6416
+ // padding, signalxjs/lynx#1084), and flush on the side its
6417
+ // placement gave up. Lynx has no descendant combinator from
6418
+ // the root, so lynx-zero stamps the root's orientation on the
6419
+ // label itself; vertical, the inset turns to the block axis.
6420
+ // Every zero is spelled through var() so it beats the
6421
+ // var-bearing inset (signalxjs/lynx#1161).
6422
+ label: {
6423
+ base: {
6424
+ paddingLeft: 'var(--space-md)',
6425
+ paddingRight: 'var(--space-md)',
6426
+ },
6427
+ selectors: {
6428
+ '&[data-placement="start"]': { paddingLeft: 'calc(var(--space-md) * 0)' },
6429
+ '&[data-placement="end"]': { paddingRight: 'calc(var(--space-md) * 0)' },
6430
+ '&[data-orientation="vertical"]': {
6431
+ paddingLeft: 'calc(var(--space-md) * 0)',
6432
+ paddingRight: 'calc(var(--space-md) * 0)',
6433
+ paddingTop: 'var(--space-sm)',
6434
+ paddingBottom: 'var(--space-sm)',
6435
+ },
6436
+ '&[data-orientation="vertical"][data-placement="start"]': { paddingTop: 'calc(var(--space-sm) * 0)' },
6437
+ '&[data-orientation="vertical"][data-placement="end"]': { paddingBottom: 'calc(var(--space-sm) * 0)' },
6438
+ },
6439
+ },
6014
6440
  },
6441
+ // A labelled divider (signalxjs/lynx#1236). With no pseudo-
6442
+ // elements, lynx-zero draws the rule's two segments as views
6443
+ // carrying the root's own line classes plus `zx-m-segment` —
6444
+ // so each paints this recipe's line, ink and thickness ramp — and
6445
+ // leaves out the one on the label's placed side (the web's
6446
+ // `:has(> [data-placement])::before { display: none }`). The root
6447
+ // holding them is stamped `zx-m-labelled` and stops being a line:
6448
+ // no fill, its cross size following the label, a row when
6449
+ // horizontal. Both are lynx-zero rendering details, not
6450
+ // modifiers an author sets, so they are styled from raw lynx css
6451
+ // (the toggle-group join ends' reasoning).
6452
+ css: `
6453
+ .zx-divider__root.zx-m-labelled {
6454
+ --divider-ink: transparent;
6455
+ }
6456
+ .zx-divider__root.zx-m-labelled.zx-o-horizontal {
6457
+ flex-direction: row;
6458
+ block-size: var(--divider-fit);
6459
+ }
6460
+ .zx-divider__root.zx-m-labelled.zx-o-vertical {
6461
+ inline-size: var(--divider-fit);
6462
+ }
6463
+ .zx-divider__root.zx-m-segment {
6464
+ flex: 1 1 0;
6465
+ }
6466
+ .zx-divider__root.zx-m-segment.zx-o-horizontal {
6467
+ min-width: 0;
6468
+ }
6469
+ .zx-divider__root.zx-m-segment.zx-o-vertical {
6470
+ min-height: 0;
6471
+ }
6472
+ `,
6015
6473
  },
6016
6474
  },
6017
6475
  };
@@ -6067,12 +6525,28 @@ export const skeleton = {
6067
6525
  keyframes: {
6068
6526
  'zero-daisyui-skeleton': 'from, to { opacity: 1; } 50% { opacity: 0.55; }',
6069
6527
  },
6528
+ // signalxjs/lynx#1237. The pulse is an opacity loop on the part itself —
6529
+ // no pseudo-element, so it carries as is; only its keyframes are
6530
+ // restated as three plain stops, not the `from, to` selector list. The
6531
+ // reduced-motion stop cannot follow: lynx emits no `@media`.
6532
+ targets: {
6533
+ lynx: {
6534
+ keyframes: {
6535
+ 'zero-daisyui-skeleton': '0% { opacity: 1; } 50% { opacity: 0.55; } 100% { opacity: 1; }',
6536
+ },
6537
+ },
6538
+ },
6070
6539
  };
6071
- /** daisy "loading-spinner": the ring, with one quadrant in the role's ink. */
6540
+ /**
6541
+ * daisy "loading-spinner": the ring, with one quadrant in the role's ink.
6542
+ * Sized on daisy's `loading-*` ramp — `--size-selector * 4…8` (16–32px), md
6543
+ * the `loading` default of `* 6` (signalxjs/lynx#1237: the ramp was
6544
+ * `--size-field * 0.35…0.9`, a 2px box that the 2px border swallowed).
6545
+ */
6072
6546
  export const spinner = {
6073
6547
  component: 'spinner',
6074
6548
  tokens: {
6075
- '--spinner-size': 'calc(var(--size-field) * 0.5)',
6549
+ '--spinner-size': 'calc(var(--size-selector) * 6)',
6076
6550
  '--spinner-ink': 'var(--color-primary)',
6077
6551
  '--spinner-track': 'var(--color-base-300)',
6078
6552
  },
@@ -6096,14 +6570,40 @@ export const spinner = {
6096
6570
  '--spinner-ink': `var(--color-${c})`,
6097
6571
  } } }])),
6098
6572
  size: {
6099
- xs: { root: { base: { '--spinner-size': 'calc(var(--size-field) * 0.35)' } } },
6100
- sm: { root: { base: { '--spinner-size': 'calc(var(--size-field) * 0.42)' } } },
6573
+ xs: { root: { base: { '--spinner-size': 'calc(var(--size-selector) * 4)' } } },
6574
+ sm: { root: { base: { '--spinner-size': 'calc(var(--size-selector) * 5)' } } },
6101
6575
  md: {},
6102
- lg: { root: { base: { '--spinner-size': 'calc(var(--size-field) * 0.7)' } } },
6103
- xl: { root: { base: { '--spinner-size': 'calc(var(--size-field) * 0.9)' } } },
6576
+ lg: { root: { base: { '--spinner-size': 'calc(var(--size-selector) * 7)' } } },
6577
+ xl: { root: { base: { '--spinner-size': 'calc(var(--size-selector) * 8)' } } },
6104
6578
  },
6105
6579
  },
6106
6580
  keyframes: { 'zero-daisyui-spin': 'to { transform: rotate(360deg); }' },
6581
+ // signalxjs/lynx#1237 — the ring in physical spellings. The logical box
6582
+ // size and `border-block-start-color` are not lynx's; the ring is
6583
+ // restated as longhands, because a var-bearing `border` shorthand
6584
+ // expands after the cascade and would repaint the head in the track
6585
+ // colour (signalxjs/lynx#1161). A flex box, since `inline-block` is not a
6586
+ // lynx display value, that keeps its size in a row. The reduced-motion
6587
+ // stop cannot follow: lynx emits no `@media`.
6588
+ targets: {
6589
+ lynx: {
6590
+ parts: {
6591
+ root: {
6592
+ base: {
6593
+ display: 'flex',
6594
+ flexShrink: '0',
6595
+ width: 'var(--spinner-size)',
6596
+ height: 'var(--spinner-size)',
6597
+ border: '0 solid transparent',
6598
+ borderStyle: 'solid',
6599
+ ...lynxSides('border{}Width', 'calc(var(--border) * 2)'),
6600
+ ...lynxSides('border{}Color', 'var(--spinner-track)', ['Right', 'Bottom', 'Left']),
6601
+ borderTopColor: 'var(--spinner-ink)',
6602
+ },
6603
+ },
6604
+ },
6605
+ },
6606
+ },
6107
6607
  };
6108
6608
  // ── The content-tier sweep (#334) ─────────────────────────────────────────
6109
6609
  /** daisy kbd: the raised base-200 cap with the doubled bottom edge. */
@@ -6149,6 +6649,35 @@ export const kbd = {
6149
6649
  xl: { root: { base: { fontSize: 'var(--text-md)', padding: 'var(--space-xs) var(--space-lg)' } } },
6150
6650
  },
6151
6651
  },
6652
+ /**
6653
+ * Lynx (signalxjs/lynx#1234). `min-inline-size` and the logical bottom
6654
+ * edge do not resolve on lynx (lynx#1145, #1084), so the cap's floor is
6655
+ * a physical `min-width` and its doubled bottom edge a physical
6656
+ * longhand; the whole hairline is longhands so a var-bearing shorthand
6657
+ * cannot expand over the thicker edge (lynx#1161/#1162). A lynx view has
6658
+ * no inline formatting context: the cap is content-sized with an
6659
+ * explicit row (lynx#1165).
6660
+ */
6661
+ targets: {
6662
+ lynx: {
6663
+ parts: {
6664
+ root: {
6665
+ base: {
6666
+ ...lynxBtnFit,
6667
+ flexDirection: 'row',
6668
+ minWidth: '1.75em',
6669
+ border: '0 solid transparent',
6670
+ ...lynxSides('border{}Width', 'var(--border)', ['Top', 'Right', 'Left']),
6671
+ borderBottomWidth: 'calc(var(--border) * 2)',
6672
+ ...lynxSides('border{}Color', 'var(--color-base-300)'),
6673
+ },
6674
+ },
6675
+ },
6676
+ variants: {
6677
+ size: { xs: { root: { base: { minWidth: '1.5em' } } } },
6678
+ },
6679
+ },
6680
+ },
6152
6681
  };
6153
6682
  /**
6154
6683
  * daisy status: the dot with the soft halo ring daisy draws around it —
@@ -6189,6 +6718,37 @@ export const status = {
6189
6718
  xl: { root: { base: { '--status-size': 'calc(var(--size-selector) * 3.5)' } } },
6190
6719
  },
6191
6720
  },
6721
+ /**
6722
+ * Lynx (signalxjs/lynx#1234). The halo mixes the recipe-local
6723
+ * `--status-ink`, which cannot bake, so it is its own token mixed from
6724
+ * each role's `--color-*` (baked per theme). `inline-size`/`block-size`
6725
+ * do not resolve on lynx (lynx#1145): the dot is sized physically, and
6726
+ * it is a flex box rather than an inline-block the engine has no
6727
+ * formatting context for.
6728
+ */
6729
+ targets: {
6730
+ lynx: {
6731
+ tokens: {
6732
+ '--status-halo': 'color-mix(in oklch, var(--color-base-content) 25%, transparent)',
6733
+ },
6734
+ parts: {
6735
+ root: {
6736
+ base: {
6737
+ display: 'flex',
6738
+ flexShrink: '0',
6739
+ width: 'var(--status-size)',
6740
+ height: 'var(--status-size)',
6741
+ boxShadow: '0 0 0 calc(var(--status-size) / 4) var(--status-halo)',
6742
+ },
6743
+ },
6744
+ },
6745
+ variants: {
6746
+ color: Object.fromEntries(ROLES.map((c) => [c, { root: { base: {
6747
+ '--status-halo': `color-mix(in oklch, var(--color-${c}) 25%, transparent)`,
6748
+ } } }])),
6749
+ },
6750
+ },
6751
+ },
6192
6752
  };
6193
6753
  /**
6194
6754
  * Indicator — pure position; the item's content brings its own paint.
@@ -6371,6 +6931,90 @@ export const stats = {
6371
6931
  xl: { value: { base: { fontSize: 'var(--text-3xl)' } } },
6372
6932
  },
6373
6933
  },
6934
+ // signalxjs/lynx#1236 — the lynx replacements for what the shared
6935
+ // sections cannot say there.
6936
+ targets: {
6937
+ lynx: {
6938
+ parts: {
6939
+ // A row unless vertical: a lynx flex container does not
6940
+ // default to one.
6941
+ root: { base: { flexDirection: 'row' } },
6942
+ // Lynx has no grid, so `display: grid` left the item in the
6943
+ // engine's default linear layout. The bands stack in a
6944
+ // centred column instead, and the figure is pinned to the
6945
+ // item's end edge (below). The padding as longhands, so the
6946
+ // `figure` rule below can widen one side by specificity (a
6947
+ // var-bearing `padding` shorthand would expand over it,
6948
+ // signalxjs/lynx#1161).
6949
+ item: {
6950
+ base: {
6951
+ display: 'flex',
6952
+ flexDirection: 'column',
6953
+ justifyContent: 'center',
6954
+ position: 'relative',
6955
+ padding: '0',
6956
+ paddingTop: 'var(--space-lg)',
6957
+ paddingBottom: 'var(--space-lg)',
6958
+ paddingLeft: 'var(--space-xl)',
6959
+ paddingRight: 'var(--space-xl)',
6960
+ },
6961
+ selectors: {
6962
+ // The seams: lynx has no sibling combinator, so the
6963
+ // web's `item + item` edge cannot be selected. Every
6964
+ // item draws its leading seam and the `first`
6965
+ // modifier takes it off the first (raw css below).
6966
+ '&[data-orientation="horizontal"]': {
6967
+ borderLeftWidth: 'var(--border)',
6968
+ borderLeftStyle: 'solid',
6969
+ borderLeftColor: 'var(--color-base-200)',
6970
+ },
6971
+ '&[data-orientation="vertical"]': {
6972
+ borderTopWidth: 'var(--border)',
6973
+ borderTopStyle: 'solid',
6974
+ borderTopColor: 'var(--color-base-200)',
6975
+ },
6976
+ },
6977
+ },
6978
+ // The web's second grid column, spanning the three bands and
6979
+ // centred on them: pinned to the item's end edge, inside its
6980
+ // padding, and centred top to bottom.
6981
+ figure: {
6982
+ base: {
6983
+ position: 'absolute',
6984
+ top: '0',
6985
+ bottom: '0',
6986
+ right: 'var(--space-xl)',
6987
+ // The web centres the grid cell with align-self; the
6988
+ // insets place it here.
6989
+ alignSelf: 'auto',
6990
+ display: 'flex',
6991
+ flexDirection: 'column',
6992
+ justifyContent: 'center',
6993
+ },
6994
+ },
6995
+ },
6996
+ // lynx-zero stamps the row's first item `zx-m-first` (mount
6997
+ // order; no `:first-child`) and an item holding a figure
6998
+ // `zx-m-figure`. Rendering details, not author modifiers, so raw
6999
+ // lynx css (the toggle-group join ends' reasoning). The first
7000
+ // item drops the leading seam, its zero width spelled through
7001
+ // var() to beat the var-bearing seam (signalxjs/lynx#1161). An
7002
+ // item with a figure keeps the figure's column clear: the
7003
+ // padding, the web's `--space-md` column gap, and a 2rem figure
7004
+ // (daisy's `stat-figure` holds a `size-8` icon or avatar).
7005
+ css: `
7006
+ .zx-stats__item.zx-m-first.zx-o-horizontal {
7007
+ border-left-width: calc(var(--border) * 0);
7008
+ }
7009
+ .zx-stats__item.zx-m-first.zx-o-vertical {
7010
+ border-top-width: calc(var(--border) * 0);
7011
+ }
7012
+ .zx-stats__item.zx-m-figure {
7013
+ padding-right: calc(var(--space-xl) + var(--space-md) + 2rem);
7014
+ }
7015
+ `,
7016
+ },
7017
+ },
6374
7018
  };
6375
7019
  /**
6376
7020
  * A timeline none of whose content sits on the start side: the root carries