@dryui/ui 1.7.4 → 1.8.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.
@@ -26,11 +26,15 @@
26
26
  | 'pill';
27
27
  size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg';
28
28
  // Autocomplete still suggests the canonical values via the literal union.
29
- color?: 'primary' | 'danger' | (string & {}) | null;
29
+ // 'ink' renders a solid near-black editorial CTA in light theme that auto-inverts
30
+ // (light bg, dark text) in dark theme, using --dry-color-{bg,text}-inverse tokens.
31
+ color?: 'primary' | 'danger' | 'ink' | (string & {}) | null;
30
32
  href?: string;
31
33
  rel?: string;
32
34
  target?: string;
33
35
  download?: boolean | string;
36
+ /** Back-compat alias for `class` — matches Heading/Text ergonomics. Prefer `class`. */
37
+ className?: HTMLButtonAttributes['class'];
34
38
  /** Callback invoked with the rendered `<button>` or `<a>` element on mount, `null` on destroy. */
35
39
  ref?: (el: HTMLButtonElement | HTMLAnchorElement | null) => void;
36
40
  children: Snippet;
@@ -52,7 +56,8 @@
52
56
  download,
53
57
  type = 'button',
54
58
  onclick,
55
- class: className,
59
+ class: classAttr,
60
+ className = classAttr,
56
61
  ref,
57
62
  children,
58
63
  ...rest
@@ -451,6 +456,30 @@
451
456
  --_dry-btn-on-accent: var(--dry-btn-on-accent, var(--dry-color-on-error));
452
457
  }
453
458
 
459
+ /* ── Ink: editorial "download / primary CTA" preset.
460
+ Uses the semantic `inverse` tokens so the surface flips between themes:
461
+ light theme → near-black bg + white text; dark theme → white bg + near-black
462
+ text. Any consumer token override (--dry-btn-bg etc.) still wins because the
463
+ variant styles read from the public layer first. */
464
+ :is(a, button)[data-color='ink'] {
465
+ --_dry-btn-accent: var(--dry-btn-accent, var(--dry-color-bg-inverse));
466
+ --_dry-btn-accent-fg: var(--dry-btn-accent-fg, var(--dry-color-text-inverse));
467
+ --_dry-btn-accent-stroke: var(--dry-btn-accent-stroke, var(--dry-color-stroke-strong));
468
+ --_dry-btn-accent-weak: var(
469
+ --dry-btn-accent-weak,
470
+ color-mix(in srgb, var(--dry-color-bg-inverse) 10%, transparent)
471
+ );
472
+ --_dry-btn-accent-hover: var(
473
+ --dry-btn-accent-hover,
474
+ color-mix(in srgb, var(--dry-color-bg-inverse) 85%, transparent)
475
+ );
476
+ --_dry-btn-accent-active: var(
477
+ --dry-btn-accent-active,
478
+ color-mix(in srgb, var(--dry-color-bg-inverse) 75%, transparent)
479
+ );
480
+ --_dry-btn-on-accent: var(--dry-btn-on-accent, var(--dry-color-text-inverse));
481
+ }
482
+
454
483
  /* ── Sizes ─────────────────────────────────────────────────────────────────── */
455
484
 
456
485
  :is(a, button)[data-size='sm'] {
@@ -3,11 +3,13 @@ import type { HTMLButtonAttributes } from 'svelte/elements';
3
3
  interface Props extends Omit<HTMLButtonAttributes, 'color'> {
4
4
  variant?: 'solid' | 'outline' | 'ghost' | 'soft' | 'secondary' | 'link' | 'bare' | 'trigger' | 'nav' | 'tab' | 'toggle' | 'pill';
5
5
  size?: 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg';
6
- color?: 'primary' | 'danger' | (string & {}) | null;
6
+ color?: 'primary' | 'danger' | 'ink' | (string & {}) | null;
7
7
  href?: string;
8
8
  rel?: string;
9
9
  target?: string;
10
10
  download?: boolean | string;
11
+ /** Back-compat alias for `class` — matches Heading/Text ergonomics. Prefer `class`. */
12
+ className?: HTMLButtonAttributes['class'];
11
13
  /** Callback invoked with the rendered `<button>` or `<a>` element on mount, `null` on destroy. */
12
14
  ref?: (el: HTMLButtonElement | HTMLAnchorElement | null) => void;
13
15
  children: Snippet;
@@ -1,10 +1,12 @@
1
1
  import type { ButtonProps as PrimitiveButtonProps } from '@dryui/primitives';
2
2
  export type ButtonVariant = 'solid' | 'outline' | 'ghost' | 'soft' | 'secondary' | 'link' | 'bare' | 'trigger' | 'nav' | 'tab' | 'toggle' | 'pill';
3
3
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'icon' | 'icon-sm' | 'icon-lg';
4
- export type ButtonColor = 'primary' | 'danger' | (string & {}) | null;
4
+ export type ButtonColor = 'primary' | 'danger' | 'ink' | (string & {}) | null;
5
5
  export interface ButtonProps extends Omit<PrimitiveButtonProps, 'color'> {
6
6
  variant?: ButtonVariant;
7
7
  size?: ButtonSize;
8
8
  color?: ButtonColor;
9
+ /** Back-compat alias for `class` — matches Heading/Text ergonomics. Prefer `class`. */
10
+ className?: string;
9
11
  }
10
12
  export { default as Button } from './button.svelte';
@@ -0,0 +1,26 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
6
+ children: Snippet;
7
+ }
8
+
9
+ let { class: className, children, ...rest }: Props = $props();
10
+ </script>
11
+
12
+ <span data-chip-group-label class={className} {...rest}>
13
+ {@render children()}
14
+ </span>
15
+
16
+ <style>
17
+ [data-chip-group-label] {
18
+ display: inline-block;
19
+ font-size: var(--dry-chip-group-label-size, var(--dry-type-tiny-size, var(--dry-text-xs-size)));
20
+ font-weight: var(--dry-chip-group-label-weight, 600);
21
+ letter-spacing: var(--dry-chip-group-label-tracking, 0.1em);
22
+ line-height: var(--dry-chip-group-label-leading, 1.2);
23
+ text-transform: uppercase;
24
+ color: var(--dry-chip-group-label-color, var(--dry-color-text-weak));
25
+ }
26
+ </style>
@@ -0,0 +1,8 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
4
+ children: Snippet;
5
+ }
6
+ declare const ChipGroupLabel: import("svelte").Component<Props, {}, "">;
7
+ type ChipGroupLabel = ReturnType<typeof ChipGroupLabel>;
8
+ export default ChipGroupLabel;
@@ -10,6 +10,8 @@
10
10
  disabled?: boolean;
11
11
  orientation?: 'horizontal' | 'vertical';
12
12
  size?: 'sm' | 'md';
13
+ gap?: 'sm' | 'md' | 'lg';
14
+ justify?: 'start' | 'center' | 'end' | 'between';
13
15
  children: Snippet;
14
16
  }
15
17
 
@@ -19,6 +21,8 @@
19
21
  disabled = false,
20
22
  orientation = 'horizontal',
21
23
  size = 'sm',
24
+ gap = 'md',
25
+ justify = 'start',
22
26
  class: className,
23
27
  children,
24
28
  ...rest
@@ -52,7 +56,7 @@
52
56
  <div
53
57
  role="group"
54
58
  data-chip-group
55
- {...variantAttrs({ orientation, size })}
59
+ {...variantAttrs({ orientation, size, gap, justify })}
56
60
  class={className}
57
61
  {...rest}
58
62
  >
@@ -60,21 +64,55 @@
60
64
  </div>
61
65
 
62
66
  <style>
67
+ /*
68
+ * ChipGroup wraps tag/chip clusters with content-driven flow.
69
+ * This is the sanctioned home for flex-wrap: the `[data-chip-group]`
70
+ * attribute is carved out of `dryui/no-flex` so chip/tag wrapping
71
+ * reads naturally without grid hacks.
72
+ */
63
73
  [data-chip-group] {
64
- display: inline-grid;
65
- grid-template-columns: repeat(auto-fill, minmax(min-content, max-content));
66
- gap: var(--dry-space-2);
74
+ display: flex;
75
+ flex-wrap: wrap;
76
+ align-items: center;
77
+ gap: var(--dry-chip-group-gap, var(--dry-space-2));
67
78
  }
68
79
 
69
- [data-chip-group][data-orientation='horizontal'] {
70
- grid-auto-flow: column;
80
+ [data-chip-group][data-orientation='vertical'] {
81
+ flex-direction: column;
82
+ align-items: flex-start;
71
83
  }
72
84
 
73
- [data-chip-group][data-orientation='vertical'] {
74
- grid-template-columns: 1fr;
75
- justify-items: start;
85
+ /* Gap presets — map onto --dry-space tokens. */
86
+ [data-chip-group][data-gap='sm'] {
87
+ --dry-chip-group-gap: var(--dry-space-1);
88
+ }
89
+
90
+ [data-chip-group][data-gap='md'] {
91
+ --dry-chip-group-gap: var(--dry-space-2);
92
+ }
93
+
94
+ [data-chip-group][data-gap='lg'] {
95
+ --dry-chip-group-gap: var(--dry-space-3);
96
+ }
97
+
98
+ /* Justify presets. */
99
+ [data-chip-group][data-justify='start'] {
100
+ justify-content: flex-start;
101
+ }
102
+
103
+ [data-chip-group][data-justify='center'] {
104
+ justify-content: center;
105
+ }
106
+
107
+ [data-chip-group][data-justify='end'] {
108
+ justify-content: flex-end;
109
+ }
110
+
111
+ [data-chip-group][data-justify='between'] {
112
+ justify-content: space-between;
76
113
  }
77
114
 
115
+ /* Size presets retained for backward-compatible Item styling. */
78
116
  [data-chip-group][data-size='sm'] {
79
117
  --dry-chip-group-font-size: var(--dry-type-tiny-size, var(--dry-text-xs-size));
80
118
  --dry-chip-group-padding-x: var(--dry-space-2_5);
@@ -6,6 +6,8 @@ interface Props extends HTMLAttributes<HTMLDivElement> {
6
6
  disabled?: boolean;
7
7
  orientation?: 'horizontal' | 'vertical';
8
8
  size?: 'sm' | 'md';
9
+ gap?: 'sm' | 'md' | 'lg';
10
+ justify?: 'start' | 'center' | 'end' | 'between';
9
11
  children: Snippet;
10
12
  }
11
13
  declare const ChipGroupRoot: import("svelte").Component<Props, {}, "value">;
@@ -1,11 +1,14 @@
1
- import type { ChipGroupRootProps as PrimitiveChipGroupRootProps } from '@dryui/primitives';
1
+ import type { ChipGroupRootProps as PrimitiveChipGroupRootProps, ChipGroupLabelProps as PrimitiveChipGroupLabelProps } from '@dryui/primitives';
2
2
  export type { ChipGroupItemProps } from '@dryui/primitives';
3
3
  export interface ChipGroupRootProps extends PrimitiveChipGroupRootProps {
4
4
  size?: 'sm' | 'md';
5
5
  }
6
+ export type ChipGroupLabelProps = PrimitiveChipGroupLabelProps;
6
7
  import ChipGroupRoot from './chip-group-root.svelte';
7
8
  import ChipGroupItem from './chip-group-button-item.svelte';
9
+ import ChipGroupLabel from './chip-group-label.svelte';
8
10
  export declare const ChipGroup: {
9
11
  Root: typeof ChipGroupRoot;
10
12
  Item: typeof ChipGroupItem;
13
+ Label: typeof ChipGroupLabel;
11
14
  };
@@ -1,6 +1,8 @@
1
1
  import ChipGroupRoot from './chip-group-root.svelte';
2
2
  import ChipGroupItem from './chip-group-button-item.svelte';
3
+ import ChipGroupLabel from './chip-group-label.svelte';
3
4
  export const ChipGroup = {
4
5
  Root: ChipGroupRoot,
5
- Item: ChipGroupItem
6
+ Item: ChipGroupItem,
7
+ Label: ChipGroupLabel
6
8
  };
@@ -6,6 +6,7 @@
6
6
  interface Props extends HTMLAttributes<HTMLHeadingElement> {
7
7
  level?: 1 | 2 | 3 | 4 | 5 | 6;
8
8
  variant?: 'default' | 'display';
9
+ maxMeasure?: 'narrow' | 'default' | 'wide' | false;
9
10
  className?: HTMLAttributes<HTMLHeadingElement>['class'];
10
11
  children: Snippet;
11
12
  }
@@ -13,35 +14,40 @@
13
14
  let {
14
15
  level = 2,
15
16
  variant = 'default',
17
+ maxMeasure = false,
16
18
  class: classAttr,
17
19
  className = classAttr,
18
20
  children,
19
21
  ...rest
20
22
  }: Props = $props();
23
+
24
+ let measure: 'narrow' | 'default' | 'wide' | undefined = $derived(
25
+ maxMeasure === false ? undefined : maxMeasure
26
+ );
21
27
  </script>
22
28
 
23
29
  {#if level === 1}
24
- <h1 class={className} {...variantAttrs({ level, variant })} {...rest}>
30
+ <h1 class={className} {...variantAttrs({ level, variant, measure })} {...rest}>
25
31
  {@render children()}
26
32
  </h1>
27
33
  {:else if level === 2}
28
- <h2 class={className} {...variantAttrs({ level, variant })} {...rest}>
34
+ <h2 class={className} {...variantAttrs({ level, variant, measure })} {...rest}>
29
35
  {@render children()}
30
36
  </h2>
31
37
  {:else if level === 3}
32
- <h3 class={className} {...variantAttrs({ level, variant })} {...rest}>
38
+ <h3 class={className} {...variantAttrs({ level, variant, measure })} {...rest}>
33
39
  {@render children()}
34
40
  </h3>
35
41
  {:else if level === 4}
36
- <h4 class={className} {...variantAttrs({ level, variant })} {...rest}>
42
+ <h4 class={className} {...variantAttrs({ level, variant, measure })} {...rest}>
37
43
  {@render children()}
38
44
  </h4>
39
45
  {:else if level === 5}
40
- <h5 class={className} {...variantAttrs({ level, variant })} {...rest}>
46
+ <h5 class={className} {...variantAttrs({ level, variant, measure })} {...rest}>
41
47
  {@render children()}
42
48
  </h5>
43
49
  {:else}
44
- <h6 class={className} {...variantAttrs({ level, variant })} {...rest}>
50
+ <h6 class={className} {...variantAttrs({ level, variant, measure })} {...rest}>
45
51
  {@render children()}
46
52
  </h6>
47
53
  {/if}
@@ -93,8 +99,24 @@
93
99
  }
94
100
 
95
101
  [data-variant='display'] {
102
+ font-family: var(--dry-font-display, var(--dry-font-sans));
96
103
  font-size: var(--dry-type-display-size, var(--dry-text-4xl-size, 2.25rem));
97
104
  line-height: var(--dry-type-display-leading, 4rem);
98
105
  letter-spacing: -0.04em;
99
106
  }
107
+
108
+ /* ── Measure (max-inline-size) ─────────────────────────────────────────────
109
+ ch-unit measure tracks text content, not viewport layout. Editorial
110
+ headlines wrap at narrow widths (~22ch) for rhythm. */
111
+ [data-measure='narrow'] {
112
+ max-inline-size: 22ch;
113
+ }
114
+
115
+ [data-measure='default'] {
116
+ max-inline-size: 45ch;
117
+ }
118
+
119
+ [data-measure='wide'] {
120
+ max-inline-size: 65ch;
121
+ }
100
122
  </style>
@@ -3,6 +3,7 @@ import type { HTMLAttributes } from 'svelte/elements';
3
3
  interface Props extends HTMLAttributes<HTMLHeadingElement> {
4
4
  level?: 1 | 2 | 3 | 4 | 5 | 6;
5
5
  variant?: 'default' | 'display';
6
+ maxMeasure?: 'narrow' | 'default' | 'wide' | false;
6
7
  className?: HTMLAttributes<HTMLHeadingElement>['class'];
7
8
  children: Snippet;
8
9
  }
@@ -3,6 +3,12 @@ import type { HTMLAttributes } from 'svelte/elements';
3
3
  export interface HeadingProps extends HTMLAttributes<HTMLHeadingElement> {
4
4
  level?: 1 | 2 | 3 | 4 | 5 | 6;
5
5
  variant?: 'default' | 'display';
6
+ /**
7
+ * Caps the rendered inline size in ch units so editorial headlines wrap on
8
+ * ergonomic measure. `false` (default) keeps the existing behaviour of no
9
+ * cap. `narrow` ≈ 22ch, `default` ≈ 45ch, `wide` ≈ 65ch.
10
+ */
11
+ maxMeasure?: 'narrow' | 'default' | 'wide' | false;
6
12
  className?: HTMLAttributes<HTMLHeadingElement>['class'];
7
13
  children: Snippet;
8
14
  }
package/dist/index.d.ts CHANGED
@@ -221,7 +221,7 @@ export type { ButtonGroupProps } from './button-group/index.js';
221
221
  export { Chip } from './chip/index.js';
222
222
  export type { ChipProps, ChipColor } from './chip/index.js';
223
223
  export { ChipGroup } from './chip-group/index.js';
224
- export type { ChipGroupRootProps, ChipGroupItemProps } from './chip-group/index.js';
224
+ export type { ChipGroupRootProps, ChipGroupItemProps, ChipGroupLabelProps } from './chip-group/index.js';
225
225
  export { Carousel } from './carousel/index.js';
226
226
  export type { CarouselRootProps, CarouselViewportProps, CarouselSlideProps, CarouselPrevProps, CarouselNextProps, CarouselDotsProps } from './carousel/index.js';
227
227
  export { FormatBytes } from './format-bytes/index.js';
@@ -7,6 +7,12 @@ export interface TextProps extends HTMLAttributes<HTMLElement> {
7
7
  font?: 'sans' | 'mono';
8
8
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
9
9
  variant?: 'default' | 'label';
10
+ /**
11
+ * Caps the rendered inline size in ch units so body copy wraps on ergonomic
12
+ * measure. `false` (default) keeps the existing behaviour of no cap.
13
+ * `narrow` ≈ 48ch, `default` ≈ 65ch, `wide` ≈ 80ch.
14
+ */
15
+ maxMeasure?: 'narrow' | 'default' | 'wide' | false;
10
16
  className?: HTMLAttributes<HTMLElement>['class'];
11
17
  children: Snippet;
12
18
  }
@@ -10,6 +10,7 @@
10
10
  font?: 'sans' | 'mono';
11
11
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
12
12
  variant?: 'default' | 'label';
13
+ maxMeasure?: 'narrow' | 'default' | 'wide' | false;
13
14
  className?: HTMLAttributes<HTMLElement>['class'];
14
15
  children: Snippet;
15
16
  }
@@ -21,23 +22,28 @@
21
22
  font = 'sans',
22
23
  weight,
23
24
  variant = 'default',
25
+ maxMeasure = false,
24
26
  class: classAttr,
25
27
  className = classAttr,
26
28
  children,
27
29
  ...rest
28
30
  }: Props = $props();
31
+
32
+ let measure: 'narrow' | 'default' | 'wide' | undefined = $derived(
33
+ maxMeasure === false ? undefined : maxMeasure
34
+ );
29
35
  </script>
30
36
 
31
37
  {#if as === 'span'}
32
38
  <span
33
39
  class={className}
34
- {...variantAttrs({ color, size, font, weight: weight || undefined, variant })}
40
+ {...variantAttrs({ color, size, font, weight: weight || undefined, variant, measure })}
35
41
  {...rest}>{@render children()}</span
36
42
  >
37
43
  {:else if as === 'div'}
38
44
  <div
39
45
  class={className}
40
- {...variantAttrs({ color, size, font, weight: weight || undefined, variant })}
46
+ {...variantAttrs({ color, size, font, weight: weight || undefined, variant, measure })}
41
47
  {...rest}
42
48
  >
43
49
  {@render children()}
@@ -45,7 +51,7 @@
45
51
  {:else}
46
52
  <p
47
53
  class={className}
48
- {...variantAttrs({ color, size, font, weight: weight || undefined, variant })}
54
+ {...variantAttrs({ color, size, font, weight: weight || undefined, variant, measure })}
49
55
  {...rest}
50
56
  >
51
57
  {@render children()}
@@ -132,4 +138,19 @@
132
138
  letter-spacing: 0.05em;
133
139
  font-weight: 600;
134
140
  }
141
+
142
+ /* ── Measure (max-inline-size) ─────────────────────────────────────────────
143
+ Body copy gets wider presets than Heading so paragraphs read well
144
+ without feeling cramped. */
145
+ [data-measure='narrow'] {
146
+ max-inline-size: 48ch;
147
+ }
148
+
149
+ [data-measure='default'] {
150
+ max-inline-size: 65ch;
151
+ }
152
+
153
+ [data-measure='wide'] {
154
+ max-inline-size: 80ch;
155
+ }
135
156
  </style>
@@ -7,6 +7,7 @@ interface Props extends HTMLAttributes<HTMLElement> {
7
7
  font?: 'sans' | 'mono';
8
8
  weight?: 'normal' | 'medium' | 'semibold' | 'bold';
9
9
  variant?: 'default' | 'label';
10
+ maxMeasure?: 'narrow' | 'default' | 'wide' | false;
10
11
  className?: HTMLAttributes<HTMLElement>['class'];
11
12
  children: Snippet;
12
13
  }
@@ -217,7 +217,15 @@
217
217
  --dry-grain-blend: soft-light;
218
218
  }
219
219
 
220
- .theme-auto {
220
+ /* When `.theme-auto` is combined with `data-theme="light"`, the explicit
221
+ `data-theme` wins — do NOT apply dark tokens under that combo, even on a
222
+ dark-preferring OS. Without this guard, `<html class="theme-auto" data-theme="light">`
223
+ on a dark OS would still resolve to dark tokens because the media query
224
+ below fires regardless of the attribute. Explicit `data-theme="dark"` is
225
+ already handled by the rule above; this class only drives the system-aware
226
+ auto behaviour when no explicit override is present (or when the override is
227
+ explicitly dark). */
228
+ .theme-auto:not([data-theme='light']) {
221
229
  @media (prefers-color-scheme: dark) {
222
230
  & {
223
231
  /* ─── Neutral ─────────────────────────────────────────────────── */
@@ -5,12 +5,13 @@
5
5
  let {
6
6
  level = 2,
7
7
  variant = 'default',
8
+ maxMeasure = false,
8
9
  class: className,
9
10
  children,
10
11
  ...rest
11
12
  }: HeadingProps = $props();
12
13
  </script>
13
14
 
14
- <Heading {level} {variant} {className} {...rest}>
15
+ <Heading {level} {variant} {maxMeasure} {className} {...rest}>
15
16
  {@render children()}
16
17
  </Heading>
@@ -9,6 +9,7 @@
9
9
  size = 'md',
10
10
  font = 'sans',
11
11
  weight,
12
+ maxMeasure = false,
12
13
  class: className,
13
14
  children,
14
15
  ...rest
@@ -20,6 +21,16 @@
20
21
  let textVariant: 'default' | 'label' = $derived(variant === 'label' ? 'label' : 'default');
21
22
  </script>
22
23
 
23
- <Text {as} color={tone} {size} {font} {weight} variant={textVariant} {className} {...rest}>
24
+ <Text
25
+ {as}
26
+ color={tone}
27
+ {size}
28
+ {font}
29
+ {weight}
30
+ variant={textVariant}
31
+ {maxMeasure}
32
+ {className}
33
+ {...rest}
34
+ >
24
35
  {@render children()}
25
36
  </Text>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dryui/ui",
3
- "version": "1.7.4",
3
+ "version": "1.8.0",
4
4
  "description": "Zero-dependency styled Svelte 5 components with scoped styles and --dry-* CSS variable theming.",
5
5
  "author": "Rob Balfre",
6
6
  "license": "MIT",
@@ -784,13 +784,13 @@
784
784
  "postpack": "bun ../../scripts/postpack-exports.ts"
785
785
  },
786
786
  "dependencies": {
787
- "@dryui/primitives": "^1.7.4"
787
+ "@dryui/primitives": "^1.8.0"
788
788
  },
789
789
  "peerDependencies": {
790
790
  "svelte": "^5.55.4"
791
791
  },
792
792
  "devDependencies": {
793
- "@dryui/lint": "^0.4.3",
793
+ "@dryui/lint": "^0.5.0",
794
794
  "svelte": "^5.55.4",
795
795
  "@sveltejs/package": "^2.5.7",
796
796
  "svelte-check": "^4.4.6",
@@ -159,6 +159,26 @@ Force a specific theme:
159
159
 
160
160
  Use `data-theme="light"` for the inverse.
161
161
 
162
+ ### Option B.1: Light-only sites
163
+
164
+ For a site that should always render light (brand / marketing / docs), combine
165
+ both attributes on `<html>`:
166
+
167
+ ```html
168
+ <html class="theme-auto" data-theme="light"></html>
169
+ ```
170
+
171
+ - `data-theme="light"` pins the page to light tokens even on a dark-preferring
172
+ OS. The `.theme-auto` dark block is guarded with
173
+ `:not([data-theme='light'])`, so a dark OS cannot silently override your
174
+ light design.
175
+ - `class="theme-auto"` stays so an opt-in dark toggle (flipping `data-theme`
176
+ to `"dark"`) keeps working. Drop `theme-auto` entirely only if you never
177
+ want a dark pathway, and the explicit `data-theme='dark']` rule is enough
178
+ on its own.
179
+
180
+ Use `ask --scope recipe "light only"` for the full scaffold.
181
+
162
182
  ### Option C: Persisted theme toggle
163
183
 
164
184
  If you add a theme switcher, keep system mode as the default and only store explicit user choices: