@sigx/zero-daisyui 0.15.0 → 0.16.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/src/recipes.ts CHANGED
@@ -1413,7 +1413,17 @@ export const tooltip: RecipeInput = {
1413
1413
  targets: {
1414
1414
  web: floatingArrow('tooltip', { background: 'var(--color-neutral)' }, '0.5rem'),
1415
1415
  lynx: {
1416
- parts: { trigger: { base: lynxBtnPad(4) } },
1416
+ parts: {
1417
+ // Content-sized like daisy's btn, never broken mid-word
1418
+ // (signalxjs/lynx#1165). No pressed rule: the anatomy
1419
+ // declares none — the long press paints the `open` state.
1420
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit } },
1421
+ // The arrow's paint: the bubble's neutral, continued. The
1422
+ // web's `floatingArrow` geometry rides runtime custom
1423
+ // properties lynx does not write, so lynx-zero places the
1424
+ // 8px square and turns it itself (signalxjs/lynx#1277).
1425
+ arrow: { base: { background: 'var(--color-neutral)' } },
1426
+ },
1417
1427
  variants: { size: lynxBtnSizes('trigger') },
1418
1428
  },
1419
1429
  },
@@ -4632,6 +4642,70 @@ export const combobox: RecipeInput = {
4632
4642
  },
4633
4643
  targets: {
4634
4644
  web: { parts: { popup: { base: anchoredListbox } } },
4645
+ // lynx-zero's combobox (signalxjs/lynx#1278): a native text field in
4646
+ // a painted `control` view, the list in the overlay outlet.
4647
+ lynx: {
4648
+ parts: {
4649
+ // select's bounds (signalxjs/lynx#1146): 20rem, never wider
4650
+ // than the container. The web's 13rem control floor
4651
+ // overflowed any cell narrower than 208px on a phone; the
4652
+ // control fills the root instead.
4653
+ root: { base: { width: '20rem', maxWidth: '100%', minWidth: '3rem' } },
4654
+ // The web `:hover` border is dropped on a touch platform. The
4655
+ // ring is the box-shadow spelling (signalxjs/lynx#1163): lynx
4656
+ // has no `outline-offset`, and its outline ignores the radius.
4657
+ control: {
4658
+ base: { minWidth: '3rem', width: '100%' },
4659
+ states: lynxFocus(),
4660
+ },
4661
+ // `color: inherit` never reaches a native text field, and
4662
+ // `::placeholder` does not exist: the ink is explicit, and the
4663
+ // placeholder takes lynx's `-x-placeholder-color` — the web's
4664
+ // 60% mix, baked per theme (input's spelling; its constant is
4665
+ // declared below this recipe, so the value is restated).
4666
+ input: {
4667
+ base: {
4668
+ color: 'var(--color-base-content)',
4669
+ '-x-placeholder-color': 'color-mix(in oklab, var(--color-base-content) 60%, transparent)',
4670
+ flexGrow: '1',
4671
+ flexShrink: '1',
4672
+ },
4673
+ },
4674
+ // The chevron and the × are `<view>`s holding a glyph: centred
4675
+ // in the stretched row (a lynx view lays its child out at the
4676
+ // top), in explicit ink. A held chevron comes up to full ink —
4677
+ // the touch answer to the web's resting 60%. The clear-trigger
4678
+ // declares no pressed flag; its feedback is the text leaving.
4679
+ trigger: {
4680
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4681
+ states: { pressed: { opacity: '1' } },
4682
+ },
4683
+ 'clear-trigger': {
4684
+ base: { color: 'var(--color-base-content)', display: 'flex', alignItems: 'center', justifyContent: 'center' },
4685
+ },
4686
+ // The chip's ring is the lynx one; its remove button's ring is
4687
+ // drawn inside, so it never paints over the chip's label.
4688
+ tag: { states: lynxFocus() },
4689
+ 'tag-remove': {
4690
+ base: { color: 'var(--color-base-content)' },
4691
+ states: lynxFocus({ inset: true }),
4692
+ },
4693
+ // The popup renders in the overlay outlet, outside the root,
4694
+ // so it cannot inherit `--combobox-accent`, and an unresolved
4695
+ // `var()` on lynx has no fallback (select's
4696
+ // signalxjs/lynx#1168). The popup restates the accent; its
4697
+ // colour-axis twins are in `variants.color` below.
4698
+ popup: { base: { '--combobox-accent': 'var(--color-primary)' } },
4699
+ // A touch list has no pointer highlight: a held option takes
4700
+ // the highlighted wash.
4701
+ item: { states: { pressed: { background: 'var(--color-base-200)' } } },
4702
+ },
4703
+ variants: {
4704
+ color: Object.fromEntries(ROLES.map((c) => [c, { popup: { base: {
4705
+ '--combobox-accent': `var(--color-${c})`,
4706
+ } } }])),
4707
+ },
4708
+ },
4635
4709
  },
4636
4710
  };
4637
4711
 
@@ -6669,7 +6743,11 @@ export const divider: RecipeInput = {
6669
6743
  // no fill, its cross size following the label, a row when
6670
6744
  // horizontal. Both are lynx-zero rendering details, not
6671
6745
  // modifiers an author sets, so they are styled from raw lynx css
6672
- // (the toggle-group join ends' reasoning).
6746
+ // (the toggle-group join ends' reasoning). A segment centres
6747
+ // itself on the label (signalxjs/lynx#1272): it carries the
6748
+ // root's `align-self: stretch`, and a stretched item with a
6749
+ // definite thickness lands at cross-start on lynx — the top of
6750
+ // the label row, not the middle daisy's rules sit on.
6673
6751
  css: `
6674
6752
  .zx-divider__root.zx-m-labelled {
6675
6753
  --divider-ink: transparent;
@@ -6683,6 +6761,7 @@ export const divider: RecipeInput = {
6683
6761
  }
6684
6762
  .zx-divider__root.zx-m-segment {
6685
6763
  flex: 1 1 0;
6764
+ align-self: center;
6686
6765
  }
6687
6766
  .zx-divider__root.zx-m-segment.zx-o-horizontal {
6688
6767
  min-width: 0;
@@ -7621,14 +7700,43 @@ export const chat: RecipeInput = {
7621
7700
  xl: { bubble: { base: { fontSize: 'var(--text-lg)', padding: 'var(--space-md) var(--space-xl)' } } },
7622
7701
  },
7623
7702
  },
7624
- // The row's vertical padding, restated physically: logical spellings
7625
- // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7626
- // the emitter refuses them.
7703
+ // Lynx (signalxjs/lynx#1276):
7704
+ // - the row's vertical padding, restated physically: logical spellings
7705
+ // resolve on iOS but not on Android (measured, signalxjs/lynx#1084), so
7706
+ // the emitter refuses them;
7707
+ // - lynx has no grid, so the row is a flex column (header, bubble,
7708
+ // footer), gathered to the placement's side. The avatar leaves the
7709
+ // flow: lynx-zero pins it to the row's bottom corner and reserves its
7710
+ // measured width beside the column;
7711
+ // - the `[data-placement] > &` selectors have no class form on the
7712
+ // parts (placement is the root's), so the bubble's square tail corner
7713
+ // is raw lynx css, a descendant of the placed root — the shape the
7714
+ // theme restatements are proven to match. Physical corners: lynx has
7715
+ // no RTL flow.
7627
7716
  targets: {
7628
7717
  lynx: {
7629
7718
  parts: {
7630
- root: { base: { paddingTop: 'var(--space-2xs)', paddingBottom: 'var(--space-2xs)' } },
7719
+ root: {
7720
+ base: {
7721
+ display: 'flex',
7722
+ flexDirection: 'column',
7723
+ alignItems: 'flex-start',
7724
+ paddingTop: 'var(--space-2xs)',
7725
+ paddingBottom: 'var(--space-2xs)',
7726
+ },
7727
+ selectors: {
7728
+ '&[data-placement="end"]': { alignItems: 'flex-end' },
7729
+ },
7730
+ },
7631
7731
  },
7732
+ css: `
7733
+ .zx-chat__root.zx-p-start .zx-chat__bubble {
7734
+ border-bottom-left-radius: 0;
7735
+ }
7736
+ .zx-chat__root.zx-p-end .zx-chat__bubble {
7737
+ border-bottom-right-radius: 0;
7738
+ }
7739
+ `,
7632
7740
  },
7633
7741
  },
7634
7742
  };
@@ -7721,6 +7829,59 @@ export const chatLog: RecipeInput = {
7721
7829
  xl: { content: { base: { rowGap: 'var(--space-md)' } }, 'jump-trigger': { base: { '--chat-log-jump-size': 'calc(var(--size-field) * 10)', fontSize: 'var(--text-sm)' } } },
7722
7830
  },
7723
7831
  },
7832
+ // Lynx (signalxjs/lynx#1276). lynx-zero's root is the frame and a native
7833
+ // scroll-view inside Content does the scrolling, so:
7834
+ // - the frame's padding moves onto the content (physical — logical
7835
+ // spellings do not resolve on Android, signalxjs/lynx#1084), so rows
7836
+ // scroll to the frame's edge instead of clipping at an inset;
7837
+ // - the content's grid is a flex column (lynx has no grid); a short
7838
+ // transcript sits at the top — `margin-block-start: auto` has nothing
7839
+ // to push against inside a scroll-view;
7840
+ // - the jump trigger floats in lynx-zero's absolute dock at the frame's
7841
+ // foot, so it drops `position: sticky` and the negative margin, and
7842
+ // restates its inline padding physically;
7843
+ // - `:hover` is dropped on touch: the held trigger shows the hover's
7844
+ // 90% darkening (restated per colour — a colour function over
7845
+ // `--chat-log-fill` cannot bake), with the existing 1px sink;
7846
+ // - the focus rings as `lynxFocus` (lynx#1163), the frame's drawn inside
7847
+ // like the web's negative offset.
7848
+ targets: {
7849
+ lynx: {
7850
+ parts: {
7851
+ root: {
7852
+ base: { padding: '0', overflow: 'hidden' },
7853
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
7854
+ },
7855
+ content: {
7856
+ base: {
7857
+ display: 'flex',
7858
+ flexDirection: 'column',
7859
+ paddingTop: 'var(--space-sm)',
7860
+ paddingBottom: 'var(--space-sm)',
7861
+ paddingLeft: 'var(--space-md)',
7862
+ paddingRight: 'var(--space-md)',
7863
+ },
7864
+ },
7865
+ 'jump-trigger': {
7866
+ base: {
7867
+ position: 'relative',
7868
+ flexDirection: 'row',
7869
+ paddingLeft: 'var(--space-md)',
7870
+ paddingRight: 'var(--space-md)',
7871
+ },
7872
+ states: {
7873
+ pressed: { background: 'color-mix(in oklab, var(--color-primary) 90%, black)' },
7874
+ ...lynxFocus(),
7875
+ },
7876
+ },
7877
+ },
7878
+ variants: {
7879
+ color: Object.fromEntries(ROLES.map((c) => [c, { 'jump-trigger': { states: {
7880
+ pressed: { background: `color-mix(in oklab, var(--color-${c}) 90%, black)` },
7881
+ } } }])),
7882
+ },
7883
+ },
7884
+ },
7724
7885
  };
7725
7886
 
7726
7887
  /** daisy radial-progress: --value became --progress-percent; same ring. */
@@ -7976,13 +8137,26 @@ export const navbar: RecipeInput = {
7976
8137
  // so the bar's height ramp is restated as `min-height`; and a lynx flex
7977
8138
  // container is only a row when it says so, so the bar and its three
7978
8139
  // sections spell `flex-direction: row`.
8140
+ //
8141
+ // No section squeezes a word (signalxjs/lynx#1274, zero#499). On the web
8142
+ // `flex: 1 1 0%` is floored by the flex item's automatic minimum (its
8143
+ // min-content width), so "Acme" never breaks. Lynx's flex layout has no
8144
+ // automatic minimum, and its `min-width` takes no intrinsic keyword, so
8145
+ // a zero basis shrank the start below its text and the `<text>` wrapped
8146
+ // per character ("Acm / e"). On lynx the ends start from their content
8147
+ // (`flex-basis: auto`), never shrink, and share the slack equally; the
8148
+ // centre takes daisy's own `flex-shrink: 0`. A bar too narrow for its
8149
+ // content overflows whole, as daisy's does — the btn rule (lynx#1165).
8150
+ // The cost: with a centre and unequal ends, the centre sits off-centre
8151
+ // by half the ends' difference (the web's is exact until a section
8152
+ // reaches its min-content).
7979
8153
  targets: {
7980
8154
  lynx: {
7981
8155
  parts: {
7982
8156
  root: { base: { flexDirection: 'row', minHeight: '4rem' } },
7983
- start: { base: { flexDirection: 'row' } },
7984
- center: { base: { flexDirection: 'row' } },
7985
- end: { base: { flexDirection: 'row' } },
8157
+ start: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
8158
+ center: { base: { flexDirection: 'row', flexShrink: '0' } },
8159
+ end: { base: { flexDirection: 'row', flexBasis: 'auto', flexShrink: '0' } },
7986
8160
  },
7987
8161
  variants: {
7988
8162
  size: {
@@ -8932,11 +9106,68 @@ export const drawer: RecipeInput = {
8932
9106
  },
8933
9107
  },
8934
9108
  },
8935
- // The btn paddings, restated physically — see `lynxBtnPad` (#1084).
9109
+ // The btn paddings, restated physically — see `lynxBtnPad` (#1084) —
9110
+ // and the btns' press rendering (`lynxBtnPressed`), as dialog wears
9111
+ // them. The edge sheet (signalxjs/lynx#1277): lynx-zero lays the
9112
+ // panel on its edge with flexbox inside the backdrop, so the sheet
9113
+ // stays in flow (`position: relative`, never `fixed`) and sizes to
9114
+ // it — no viewport units, which lynx does not resolve against the
9115
+ // overlay outlet. The skin keeps the cap, the corners and the slide.
8936
9116
  lynx: {
9117
+ keyframes: {
9118
+ // `translate` is Android-asymmetric (#1084): the travel is a
9119
+ // transform function, one per edge.
9120
+ 'zero-daisy-drawer-in-start': 'from { transform: translateX(-100%); } to { transform: translateX(0); }',
9121
+ 'zero-daisy-drawer-in-end': 'from { transform: translateX(100%); } to { transform: translateX(0); }',
9122
+ 'zero-daisy-drawer-in-top': 'from { transform: translateY(-100%); } to { transform: translateY(0); }',
9123
+ 'zero-daisy-drawer-in-bottom': 'from { transform: translateY(100%); } to { transform: translateY(0); }',
9124
+ },
8937
9125
  parts: {
8938
- trigger: { base: lynxBtnPad(4) },
8939
- close: { base: lynxBtnPad(4) },
9126
+ trigger: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
9127
+ close: { base: { ...lynxBtnPad(4), ...lynxBtnFit }, states: { pressed: lynxBtnPressed, ...lynxFocus() } },
9128
+ panel: {
9129
+ // The cap alone: `min(20rem, 85vw)` has no lynx spelling
9130
+ // (no min()); lynx-zero states the 85% share inline and
9131
+ // this caps it — together, the web's width.
9132
+ base: { '--l-measure': '20rem' },
9133
+ selectors: {
9134
+ '&[data-l-dock="sheet"]': {
9135
+ position: 'relative',
9136
+ blockSize: 'auto',
9137
+ maxBlockSize: 'none',
9138
+ },
9139
+ // The block edges run full width (the web's `:where()`
9140
+ // pair, one selector each — no `:where()` on lynx),
9141
+ // content-sized up to 85% of the outlet.
9142
+ '&[data-placement="top"]': { '--l-measure': 'none' },
9143
+ '&[data-placement="bottom"]': { '--l-measure': 'none' },
9144
+ '&[data-placement="top"][data-l-dock="sheet"]': {
9145
+ maxBlockSize: '85%',
9146
+ borderBottomLeftRadius: 'var(--radius-box)',
9147
+ borderBottomRightRadius: 'var(--radius-box)',
9148
+ },
9149
+ '&[data-placement="bottom"][data-l-dock="sheet"]': {
9150
+ maxBlockSize: '85%',
9151
+ borderTopLeftRadius: 'var(--radius-box)',
9152
+ borderTopRightRadius: 'var(--radius-box)',
9153
+ },
9154
+ // The slide in from the edge (the web's `translate`
9155
+ // over `@starting-style`, both dropped here). The
9156
+ // exit is an unmount on lynx, so only the entry runs.
9157
+ '&[data-placement="start"][data-l-dock="sheet"][data-state="open"]': {
9158
+ animation: 'zero-daisy-drawer-in-start var(--duration-slow) var(--ease-standard)',
9159
+ },
9160
+ '&[data-placement="end"][data-l-dock="sheet"][data-state="open"]': {
9161
+ animation: 'zero-daisy-drawer-in-end var(--duration-slow) var(--ease-standard)',
9162
+ },
9163
+ '&[data-placement="top"][data-l-dock="sheet"][data-state="open"]': {
9164
+ animation: 'zero-daisy-drawer-in-top var(--duration-slow) var(--ease-standard)',
9165
+ },
9166
+ '&[data-placement="bottom"][data-l-dock="sheet"][data-state="open"]': {
9167
+ animation: 'zero-daisy-drawer-in-bottom var(--duration-slow) var(--ease-standard)',
9168
+ },
9169
+ },
9170
+ },
8940
9171
  },
8941
9172
  variants: { size: lynxBtnSizes('trigger') },
8942
9173
  },
@@ -9136,6 +9367,89 @@ export const table: RecipeInput = {
9136
9367
  },
9137
9368
  },
9138
9369
  },
9370
+ // signalxjs/lynx#1279. Lynx has no table layout: lynx-zero lays the
9371
+ // parts out as flex rows itself. The `hover` modifier has no lynx
9372
+ // answer (a touch screen has no hover), nor has the viewport-width
9373
+ // `@media` of the stacked mode — lynx-zero resolves `stack` in JS and
9374
+ // stamps the parts instead (see `css` below).
9375
+ targets: {
9376
+ lynx: {
9377
+ parts: {
9378
+ // The web's `overflow-x: auto` box: a lynx view clips its
9379
+ // rounded frame; a table of fixed columns scrolls in a
9380
+ // scroll-view lynx-zero renders.
9381
+ root: {
9382
+ base: { overflow: 'hidden' },
9383
+ // daisy's ring drawn inside the box, as on the web
9384
+ // (outline-offset: -2px) — spelled as an inset shadow.
9385
+ states: { 'focus-visible': lynxFocusRing('var(--color-base-content)', { inset: true }) },
9386
+ },
9387
+ // The muted header ink: the colour function over
9388
+ // `--table-accent` cannot bake (a recipe-local property),
9389
+ // so the default accent's mix is restated here and each
9390
+ // colour's below.
9391
+ 'header-cell': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9392
+ 'cell-label': { base: { color: 'color-mix(in oklab, var(--color-base-content) 60%, transparent)' } },
9393
+ // The web's hover ink, while held; the lynx ring.
9394
+ 'sort-trigger': {
9395
+ base: { flexDirection: 'row' },
9396
+ states: { pressed: { color: 'var(--table-accent)' }, ...lynxFocus() },
9397
+ },
9398
+ },
9399
+ variants: {
9400
+ color: Object.fromEntries(ROLES.map((c) => {
9401
+ const muted = `color-mix(in oklab, ${roleInk(c)} 60%, transparent)`;
9402
+ return [c, {
9403
+ 'header-cell': { base: { color: muted } },
9404
+ 'cell-label': { base: { color: muted } },
9405
+ }];
9406
+ })),
9407
+ },
9408
+ // lynx-zero rendering details, not modifiers an author sets:
9409
+ // - `stripe`: lynx has no `:nth-child`, so the body stamps its
9410
+ // even, unselected rows (mount order) under `zebra`.
9411
+ // - `held`: the unsorted mark the web reveals under a hovered or
9412
+ // keyboard-focused trigger, shown while the trigger is held.
9413
+ // - `stacked`: the stacked mode's card chrome (the web's
9414
+ // below-breakpoint rules). Zero widths and paddings keep a
9415
+ // var() so they beat the var-bearing base values (lynx expands
9416
+ // var() declarations after the cascade, signalxjs/lynx#1161);
9417
+ // the root keeps its base-100 fill (a var-free transparent
9418
+ // would lose the same way).
9419
+ css: `
9420
+ .zx-table__root.zx-m-stacked {
9421
+ border-width: calc(var(--border) * 0);
9422
+ border-radius: calc(var(--radius-box) * 0);
9423
+ }
9424
+ .zx-table__caption.zx-m-stacked {
9425
+ padding-left: calc(var(--table-pad-inline) * 0);
9426
+ padding-right: calc(var(--table-pad-inline) * 0);
9427
+ }
9428
+ .zx-table__row.zx-m-stacked {
9429
+ border: var(--border) solid var(--color-base-200);
9430
+ border-radius: var(--radius-box);
9431
+ background: var(--color-base-100);
9432
+ padding: var(--space-sm) var(--table-pad-inline);
9433
+ margin-bottom: var(--space-md);
9434
+ }
9435
+ .zx-table__row.zx-m-stacked.zx-f-selected {
9436
+ background: var(--color-base-300);
9437
+ }
9438
+ .zx-table__cell.zx-m-stacked {
9439
+ padding-left: calc(var(--table-pad-inline) * 0);
9440
+ padding-right: calc(var(--table-pad-inline) * 0);
9441
+ padding-top: var(--space-xs);
9442
+ padding-bottom: var(--space-xs);
9443
+ }
9444
+ .zx-table__row.zx-m-stripe {
9445
+ background: var(--color-base-200);
9446
+ }
9447
+ .zx-table__sort-indicator.zx-m-held.zx-s-none {
9448
+ opacity: 0.6;
9449
+ }
9450
+ `,
9451
+ },
9452
+ },
9139
9453
  };
9140
9454
 
9141
9455
  /**
@@ -9302,6 +9616,65 @@ export const fileUpload: RecipeInput = {
9302
9616
  xl: { root: { base: { '--fu-pad': 'calc(var(--space-xl) * 1.25)', '--fu-font': 'var(--text-md)' } } },
9303
9617
  },
9304
9618
  },
9619
+ // Lynx (signalxjs/lynx#1276):
9620
+ // - lynx has no grid: the root and the item list are flex columns, and
9621
+ // the stretched parts (dropzone, item list) say so with `align-self`;
9622
+ // the rows (trigger, item) state `flex-direction: row`;
9623
+ // - the dropzone centres its words in a column (lynx text alignment is
9624
+ // the text's own), and its highlighted wash — a colour function over
9625
+ // `--fu-accent`, which cannot bake — is restated per colour over the
9626
+ // role. lynx never drives `highlighted` (no drag source); it shows
9627
+ // through `ForceStates` in the gallery;
9628
+ // - `:hover` is dropped on touch: the held trigger shows daisy's
9629
+ // base-300 wash + 1px sink (`lynxBtnPressed`, in place of the
9630
+ // standalone `translate`, which Android does not resolve), and the
9631
+ // held × and clear show the ghost's base-200 wash;
9632
+ // - the focus rings as `lynxFocus` (lynx#1163).
9633
+ targets: {
9634
+ lynx: {
9635
+ parts: {
9636
+ root: { base: { display: 'flex', flexDirection: 'column', alignItems: 'flex-start' } },
9637
+ trigger: {
9638
+ base: { flexDirection: 'row' },
9639
+ states: { pressed: lynxBtnPressed, ...lynxFocus() },
9640
+ },
9641
+ dropzone: {
9642
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column', alignItems: 'center' },
9643
+ states: {
9644
+ highlighted: { background: 'color-mix(in oklab, var(--color-primary) 8%, var(--color-base-100))' },
9645
+ },
9646
+ },
9647
+ 'item-group': {
9648
+ base: { alignSelf: 'stretch', display: 'flex', flexDirection: 'column' },
9649
+ },
9650
+ item: { base: { flexDirection: 'row' } },
9651
+ 'item-remove': {
9652
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9653
+ },
9654
+ 'clear-trigger': {
9655
+ states: { pressed: { background: 'var(--color-base-200)' }, ...lynxFocus() },
9656
+ },
9657
+ },
9658
+ variants: {
9659
+ color: Object.fromEntries(ROLES.map((c) => [c, { dropzone: { states: {
9660
+ highlighted: { background: `color-mix(in oklab, var(--color-${c}) 8%, var(--color-base-100))` },
9661
+ } } }])),
9662
+ },
9663
+ },
9664
+ },
9665
+ };
9666
+
9667
+ /** A carousel nav circle on lynx: the glyph centred as flex (no `grid`). */
9668
+ const carouselTriggerLynx: CssProps = {
9669
+ display: 'flex',
9670
+ alignItems: 'center',
9671
+ justifyContent: 'center',
9672
+ };
9673
+
9674
+ /** A held carousel nav circle on lynx: daisy's hover wash plus the 1px sink, and the lynx ring. */
9675
+ const carouselTriggerLynxStates: Record<string, CssProps> = {
9676
+ pressed: { background: 'var(--color-base-300)', transform: 'translateY(1px)' },
9677
+ ...lynxFocus(),
9305
9678
  };
9306
9679
 
9307
9680
  /**
@@ -9454,14 +9827,60 @@ export const carousel: RecipeInput = {
9454
9827
  // spellings resolve on iOS but not on Android (measured,
9455
9828
  // signalxjs/lynx#1084), so the emitter refuses them. Physical is the
9456
9829
  // lynx target's norm — no RTL flow there, so `start` IS left.
9830
+ //
9831
+ // signalxjs/lynx#1279 — the rest of the lynx shape:
9832
+ // - `grid` does not lay out on lynx: the root is a flex column, and a
9833
+ // trigger centres its glyph as flex.
9834
+ // - The dot: the web draws it on `::before` inside a
9835
+ // `max(dot, 1.5rem)` button; lynx has neither pseudo-elements nor
9836
+ // `max()`, so lynx-zero renders the dot AS the indicator (its touch
9837
+ // area widened with `hit-slop`) and the ring and the active fill ride
9838
+ // the part.
9839
+ // - The web sinks a held trigger with the standalone `translate`,
9840
+ // which Android ignores (#1084): a transform function here, over
9841
+ // daisy's hover wash (no hover on touch).
9842
+ // - Rings: lynx's outline ignores radius and has no offset (#1163).
9843
+ // No replacement for the viewport's `:focus-visible` ring: there is no
9844
+ // keyboard focus on lynx, and the part declares no flag to key it on.
9457
9845
  targets: {
9458
9846
  lynx: {
9459
9847
  parts: {
9848
+ root: { base: { display: 'flex', flexDirection: 'column' } },
9460
9849
  'prev-trigger': {
9461
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', left: 'var(--space-sm)' },
9850
+ base: {
9851
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9852
+ left: 'var(--space-sm)',
9853
+ ...carouselTriggerLynx,
9854
+ },
9855
+ states: carouselTriggerLynxStates,
9462
9856
  },
9463
9857
  'next-trigger': {
9464
- base: { top: 'calc(50% - var(--carousel-nav) / 2)', right: 'var(--space-sm)' },
9858
+ base: {
9859
+ top: 'calc(50% - var(--carousel-nav) / 2)',
9860
+ right: 'var(--space-sm)',
9861
+ ...carouselTriggerLynx,
9862
+ },
9863
+ states: carouselTriggerLynxStates,
9864
+ },
9865
+ 'indicator-group': { base: { flexDirection: 'row', alignItems: 'center' } },
9866
+ indicator: {
9867
+ base: {
9868
+ display: 'flex',
9869
+ width: 'var(--carousel-dot)',
9870
+ height: 'var(--carousel-dot)',
9871
+ flexShrink: '0',
9872
+ // Longhands: the ring's colour bakes per theme only
9873
+ // on its own (a shorthand mixing it with the
9874
+ // --border calc cannot).
9875
+ borderWidth: 'calc(var(--border) * 2)',
9876
+ borderStyle: 'solid',
9877
+ borderColor: 'color-mix(in oklab, var(--color-base-content) 70%, transparent)',
9878
+ borderRadius: '9999px',
9879
+ },
9880
+ states: {
9881
+ active: { background: 'var(--carousel-accent)', borderColor: 'var(--carousel-accent)' },
9882
+ ...lynxFocus(),
9883
+ },
9465
9884
  },
9466
9885
  },
9467
9886
  },