@marianmeres/stuic 3.143.0 → 3.145.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/AGENTS.md CHANGED
@@ -23,7 +23,7 @@
23
23
 
24
24
  ```
25
25
  src/lib/
26
- ├── components/ # 57 component directories
26
+ ├── components/ # 63 component directories
27
27
  ├── actions/ # 15 Svelte actions (use: directives)
28
28
  ├── attachments/ # Svelte attachments ({@attach} — preferred for new DOM helpers)
29
29
  ├── utils/ # 44 utility modules
@@ -53,6 +53,9 @@ Theme CSS files are not bundled in this package — they're provided by `@marian
53
53
  5. Create components without `unstyled`, `class`, `el` props
54
54
  6. Use `dark:` Tailwind prefix when CSS vars handle dark mode
55
55
  7. Import CSS inside components — centralize in `src/lib/index.css`
56
+ (**exception:** subpath-export components with optional peer deps — `MarkdownEditor`,
57
+ `CommentInput` — import their own `index.css` locally so their styles don't ship to
58
+ barrel-only consumers. Enforced by `src/lib/barrel-optional-peers.test.ts`.)
56
59
  8. Declare component tokens at `:root` that reference shared structural tokens (use fallback pattern instead)
57
60
 
58
61
  ### CSS Variable Pattern
@@ -125,7 +128,7 @@ Global tokens that control cross-component visual properties. Defined in `src/li
125
128
 
126
129
  ### Domain Docs
127
130
 
128
- - [Components](./docs/domains/components.md) — 57 component directories, Props pattern, snippets
131
+ - [Components](./docs/domains/components.md) — 63 component directories, Props pattern, snippets
129
132
  - [Theming](./docs/domains/theming.md) — CSS tokens, dark mode, themes
130
133
  - [Actions](./docs/domains/actions.md) — 15 Svelte directives
131
134
  - [Attachments](./docs/domains/attachments.md) — `{@attach}` DOM helpers (preferred for new ones)
package/API.md CHANGED
@@ -2178,6 +2178,7 @@ Each component defines customization tokens. Override globally in `:root {}` or
2178
2178
  | ------------------ | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
2179
2179
  | Button | `--stuic-button-*` | `bg`, `text`, `border`, `ring-color`, `radius`, `padding-x-{size}` |
2180
2180
  | Switch | `--stuic-switch-*` | `accent` |
2181
+ | Slider | `--stuic-slider-*` | `track`, `fill`, `thumb`, `tick`, `tick-on-fill`, `thickness`, `length`, `radius`, `fill-radius` |
2181
2182
  | Input | `--stuic-input-*` | `accent`, `accent-error` |
2182
2183
  | Progress | `--stuic-progress-*` | `bg`, `accent` |
2183
2184
  | ListItemButton | `--stuic-list-item-button-*` | `bg`, `text`, `border`, `bg-hover`, `text-hover` |
package/README.md CHANGED
@@ -27,6 +27,29 @@ npm install @marianmeres/stuic
27
27
  </Modal>
28
28
  ```
29
29
 
30
+ ## Subpath exports
31
+
32
+ Most of the library is on the main entry. A few things live behind subpaths:
33
+
34
+ | Subpath | Contents |
35
+ | ------------------------------------- | ------------------------------------------------------------- |
36
+ | `@marianmeres/stuic` | Components, actions, icons, utils — everything below excepted |
37
+ | `@marianmeres/stuic/utils` | Utilities only, without pulling in components |
38
+ | `@marianmeres/stuic/phone-validation` | Phone validation helpers |
39
+ | `@marianmeres/stuic/markdown-editor` | `MarkdownEditor` — requires optional peer deps |
40
+ | `@marianmeres/stuic/comment-input` | `CommentInput` — requires optional peer deps |
41
+
42
+ The last two are **not** on the main entry by design: they depend on Milkdown and
43
+ CodeMirror, which are declared as _optional_ peer dependencies. Keeping them off the
44
+ barrel means consumers who don't use them never have to install that stack — and,
45
+ more importantly, their builds don't fail for want of it.
46
+
47
+ ```ts
48
+ import { CommentInput } from "@marianmeres/stuic/comment-input";
49
+ ```
50
+
51
+ See each component's README for the peer set to install.
52
+
30
53
  ## Theming System
31
54
 
32
55
  STUIC uses a 4-layer CSS variable token system:
@@ -152,7 +175,7 @@ FieldInput, FieldMoney, FieldTextarea, FieldSelect, FieldCheckbox, FieldRadios,
152
175
 
153
176
  ### Buttons & Controls
154
177
 
155
- Button, ButtonGroupRadio, Switch, TwCheck, ListItemButton, X
178
+ Button, ButtonGroupRadio, Switch, Slider, TwCheck, ListItemButton, X
156
179
 
157
180
  ### Feedback & Notifications
158
181
 
@@ -143,6 +143,7 @@
143
143
  import { Button } from "../Button/index.js";
144
144
  import Thc from "../Thc/Thc.svelte";
145
145
  import { MarkdownEditor } from "../MarkdownEditor/index.js";
146
+ import "./index.css";
146
147
 
147
148
  let {
148
149
  value = $bindable(""),
@@ -110,6 +110,7 @@ export interface Props extends InputWrapClassProps {
110
110
  classFooter?: string;
111
111
  style?: string;
112
112
  }
113
+ import "./index.css";
113
114
  declare const CommentInput: import("svelte").Component<Props, {
114
115
  validate: () => ValidationResult | undefined;
115
116
  clearValidation: () => void;
@@ -12,9 +12,14 @@ lightweight, opinionated wrapper for the common "leave a comment / reply" use
12
12
  case: a clean minimal toolbar, an avatar, a submit button, and
13
13
  ⌘/Ctrl+Enter-to-send — without you having to wire any of it up.
14
14
 
15
+ Because it embeds `MarkdownEditor`, it is shipped as an **optional subpath export**
16
+ (`@marianmeres/stuic/comment-input`), separate from the main barrel — so the heavy
17
+ editor dependencies never reach consumers who don't use it. Importing it from
18
+ `@marianmeres/stuic` will not work.
19
+
15
20
  ```svelte
16
21
  <script>
17
- import { CommentInput } from "@marianmeres/stuic";
22
+ import { CommentInput } from "@marianmeres/stuic/comment-input";
18
23
 
19
24
  let value = $state("");
20
25
 
@@ -49,6 +54,11 @@ This is the same peer set as `MarkdownEditor` — see its README for the
49
54
  authoritative list. If they aren't installed, the surface stays empty and a
50
55
  console error explains why.
51
56
 
57
+ > **CSS is imported locally** by this component, not via the central
58
+ > `src/lib/index.css`. Deliberate deviation from the usual STUIC convention,
59
+ > required so these styles ship only to subpath users. Same as `MarkdownEditor`;
60
+ > see `index.css` for the rationale.
61
+
52
62
  ## Toolbar
53
63
 
54
64
  `toolbar` accepts `true` (the default), `false` (hidden), or an ordered
@@ -1,6 +1,19 @@
1
1
  /******************************************************************************
2
2
  CommentInput
3
3
 
4
+ NOTE — intentional deviation from the usual STUIC CSS convention.
5
+ Every other component's `index.css` is `@import`-ed into the central
6
+ `src/lib/index.css`. This component is NOT: because it embeds `MarkdownEditor`
7
+ it is an OPTIONAL subpath export (`@marianmeres/stuic/comment-input`) kept off
8
+ the main barrel, so the heavy Milkdown/CodeMirror peers never reach consumers
9
+ who don't use it. Centralizing its CSS would ship these styles to every
10
+ consumer anyway. Instead `CommentInput.svelte` imports THIS file locally; the
11
+ package's `sideEffects` glob (all css files) keeps the import for real users.
12
+
13
+ `MarkdownEditor/index.css` arrives on its own — `MarkdownEditor.svelte`
14
+ imports it locally too — so do NOT `@import` it here (it would double-inject
15
+ for anyone using both components).
16
+
4
17
  A GitHub-style comment composer built on top of `MarkdownEditor` (a rich
5
18
  WYSIWYG / source surface) with an avatar gutter and a submit/cancel footer.
6
19
 
@@ -0,0 +1,205 @@
1
+ # Slider
2
+
3
+ A fancy `input[type="range"]` wrap — a pill-shaped track that fills with the value, with
4
+ an optional icon-capable thumb riding the fill edge (think iOS volume control). Supports
5
+ horizontal and vertical orientation, pointer dragging with step snapping, native keyboard
6
+ interaction, tick marks, a floating value label, form participation, and validation.
7
+
8
+ Not a replacement for `FieldInput type="range"` — this is the special-case "fancy"
9
+ variant for custom UI (volume/brightness controls, dashboards, media players).
10
+
11
+ ## Props
12
+
13
+ | Prop | Type | Default | Description |
14
+ | --------------- | ------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
15
+ | `value` | `number` | `min` | Current value (bindable; non-finite / out-of-range / off-grid writes are normalized back) |
16
+ | `min` | `number` | `0` | Minimum value |
17
+ | `max` | `number` | `100` | Maximum value |
18
+ | `step` | `number \| "any"` | `1` | Snap increment (`"any"` or non-positive = continuous) |
19
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Slider direction (vertical fills bottom-up) |
20
+ | `size` | `"sm" \| "md" \| "lg" \| string` | `"md"` | Cross-axis thickness preset |
21
+ | `intent` | `"primary" \| "accent" \| "success" \| "warning" \| "destructive"` | - | Semantic fill color |
22
+ | `thumb` | `boolean \| Snippet<[SliderRenderCtx]>` | `true` | `false` hides the thumb (fill-only look), snippet renders inside thumb |
23
+ | `thumbPosition` | `"value" \| "start"` | `"value"` | `"value"` rides the fill edge; `"start"` pins it to the left/bottom so only the bar moves (true iOS volume look) |
24
+ | `fillRounded` | `boolean` | `false` | Round the fill's leading edge ("pill inside a pill") |
25
+ | `ticks` | `boolean \| number[]` | - | `true` = tick at every `step` (positive numeric step only; skipped above 101 auto ticks — pass an array), array = ticks at given in-range values |
26
+ | `valueLabel` | `Snippet<[SliderRenderCtx]>` | - | Floating label at the current value along the track |
27
+ | `disabled` | `boolean` | `false` | Disable interaction |
28
+ | `label` | `string` | - | Screen reader label for the underlying input |
29
+ | `id` | `string` | - | Id for the underlying input (enables `<label for>` association) |
30
+ | `name` | `string` | - | Form field name for the hidden range input |
31
+ | `required` | `boolean` | `false` | Forwarded to the input; per HTML spec inert on range inputs (use `validate.customValidator` for custom rules) |
32
+ | `oninput` | `(value: number) => void` | - | Fires on every value change (drag, keyboard) |
33
+ | `onchange` | `(value: number) => void` | - | Fires when a change is committed (drag release, keyboard) |
34
+ | `validate` | `boolean \| ValidateOptions` | - | Enable validation (stuic validate action) |
35
+ | `unstyled` | `boolean` | `false` | Skip all default styling |
36
+ | `class` | `string` | - | Classes for the root element |
37
+ | `trackClass` | `string` | - | Classes for the track (pill background) |
38
+ | `fillClass` | `string` | - | Classes for the fill (value indicator) |
39
+ | `thumbClass` | `string` | - | Classes for the thumb |
40
+ | `tickClass` | `string` | - | Classes for each tick mark |
41
+ | `valueClass` | `string` | - | Classes for the value label wrapper |
42
+ | `el` | `HTMLDivElement` | - | Root element reference (bindable) |
43
+ | `inputEl` | `HTMLInputElement` | - | Hidden range input reference (bindable) |
44
+
45
+ `SliderRenderCtx` (passed to the `thumb` and `valueLabel` snippets):
46
+ `{ value: number; ratio: number /* 0..1 */; percent: number /* 0..100 */; dragging: boolean }`
47
+
48
+ Remaining props are spread onto the root `<div>`.
49
+
50
+ ### Exported methods (via component instance binding)
51
+
52
+ | Method | Description |
53
+ | ----------------------- | ----------------------------------- |
54
+ | `validate()` | Trigger validation now |
55
+ | `clearValidation()` | Clear the current validation result |
56
+ | `getValidation()` | Read the current validation result |
57
+ | `focus()` | Focus the underlying range input |
58
+ | `scrollIntoView(opts?)` | Scroll the slider into view |
59
+
60
+ ## Usage
61
+
62
+ ### Basic
63
+
64
+ ```svelte
65
+ <script lang="ts">
66
+ import { Slider } from "@marianmeres/stuic";
67
+
68
+ let volume = $state(30);
69
+ </script>
70
+
71
+ <Slider bind:value={volume} />
72
+ ```
73
+
74
+ ### Vertical volume control (thumb rides the fill edge)
75
+
76
+ ```svelte
77
+ <Slider
78
+ orientation="vertical"
79
+ size="lg"
80
+ bind:value={volume}
81
+ style="--stuic-slider-length: 10rem; --stuic-slider-fill: white; --stuic-slider-track: rgb(255 255 255 / .25);"
82
+ >
83
+ {#snippet thumb()}
84
+ {@html iconVolume({ size: 16 })}
85
+ {/snippet}
86
+ </Slider>
87
+ ```
88
+
89
+ ### Fill-only (no thumb)
90
+
91
+ ```svelte
92
+ <Slider thumb={false} bind:value={brightness} />
93
+ ```
94
+
95
+ ### True iOS volume: fixed icon at the start, only the bar moves
96
+
97
+ ```svelte
98
+ <!-- bare icon (no knob): drop the thumb's own background/shadow -->
99
+ <Slider
100
+ orientation="vertical"
101
+ thumbPosition="start"
102
+ fillRounded
103
+ thumbClass="bg-transparent shadow-none"
104
+ bind:value={volume}
105
+ >
106
+ {#snippet thumb()}
107
+ {@html iconVolume({ size: 18 })}
108
+ {/snippet}
109
+ </Slider>
110
+
111
+ <!-- or keep the white knob pinned at the bottom -->
112
+ <Slider orientation="vertical" thumbPosition="start" bind:value={volume}>
113
+ {#snippet thumb()}
114
+ {@html iconVolume({ size: 16 })}
115
+ {/snippet}
116
+ </Slider>
117
+ ```
118
+
119
+ With `thumbPosition="start"` the thumb is decorative (`pointer-events: none`) and the
120
+ value maps linearly across the full track — there is no thumb-travel inset.
121
+
122
+ ### Steps and ticks
123
+
124
+ ```svelte
125
+ <Slider min={0} max={10} step={1} ticks bind:value={rating} />
126
+ <Slider min={0} max={100} step={0.5} ticks={[0, 25, 50, 75, 100]} />
127
+ ```
128
+
129
+ ### Value label
130
+
131
+ ```svelte
132
+ <Slider bind:value={percent}>
133
+ {#snippet valueLabel({ value })}
134
+ {value}%
135
+ {/snippet}
136
+ </Slider>
137
+ ```
138
+
139
+ ### Continuous (no snapping)
140
+
141
+ ```svelte
142
+ <Slider step="any" bind:value={gain} />
143
+ ```
144
+
145
+ ### In a form
146
+
147
+ ```svelte
148
+ <form onsubmit={...}>
149
+ <Slider name="volume" bind:value={volume} />
150
+ </form>
151
+ ```
152
+
153
+ ## Interaction
154
+
155
+ - **Pointer**: press anywhere on the track to jump the value there, then drag. Grabbing
156
+ the thumb itself does not jump (drag continues from the grab point).
157
+ - **Keyboard**: focus and use Arrow keys / PageUp / PageDown / Home / End — native
158
+ `input[type=range]` behavior (the real input is visually hidden but focusable).
159
+ - **Vertical**: bottom is `min`, top is `max`; ArrowUp increases.
160
+ - **RTL**: horizontal sliders flip automatically (logical CSS properties + pointer math).
161
+ - **Commit semantics**: `oninput` fires only on actual value changes; `onchange` only
162
+ when a drag/keypress committed a _different_ value (native-faithful — a no-move tap
163
+ fires neither).
164
+ - **Off-grid max**: when `max` is not on the step grid (e.g. `min=0 max=95 step=10`),
165
+ the largest reachable value is the last grid point (`90`), matching native range
166
+ sanitization.
167
+
168
+ ## Caveats
169
+
170
+ - **Cross-axis sizing**: size the thickness via `size` presets or
171
+ `--stuic-slider-thickness` — not via `h-*`/`w-*` utility classes. The pointer math
172
+ and the CSS thumb positioning both derive from the thickness; a utility class
173
+ resizes the box without updating `--_thickness`, misaligning them. (Main-axis
174
+ length via a class — e.g. `class="h-40"` on a vertical slider — is fine.)
175
+ - **Root pointer handlers are reserved**: `onpointerdown/move/up/cancel` are excluded
176
+ from `Props` (the drag machinery owns them). Wrap the slider if you need them.
177
+ - **Touch**: the slider claims the whole touch gesture (`touch-action: none`) —
178
+ a touch starting on it adjusts the value and never scrolls the page.
179
+ - **The wrapper is not the control**: props spread onto the root `<div>` — including
180
+ `aria-*` and `onfocus`/`onblur` — never reach the underlying `<input type="range">`
181
+ and are inert for assistive tech. Use `label` (→ `aria-label`) and `id` (→ `<label for>`);
182
+ for anything else, bind `inputEl` and wire it imperatively.
183
+
184
+ ## CSS Variables
185
+
186
+ | Variable | Default | Description |
187
+ | --------------------------------- | -------------------------- | --------------------------------- |
188
+ | `--stuic-slider-track` | `--stuic-color-muted` | Track (pill background) color |
189
+ | `--stuic-slider-fill` | `--stuic-color-primary` | Fill color |
190
+ | `--stuic-slider-thumb` | `--color-white` | Thumb background |
191
+ | `--stuic-slider-thumb-foreground` | `--stuic-color-foreground` | Thumb content color |
192
+ | `--stuic-slider-tick` | foreground 25% mix | Tick mark color (over the track) |
193
+ | `--stuic-slider-tick-on-fill` | background 55% mix | Tick mark color (over the fill) |
194
+ | `--stuic-slider-ring-width` | `4px` | Focus ring width |
195
+ | `--stuic-slider-ring-color` | `--stuic-color-ring` | Focus ring color |
196
+ | `--stuic-slider-thickness` | `2rem` (`sm` 1.25, `lg` 3) | Cross-axis size |
197
+ | `--stuic-slider-length` | `10rem` | Main-axis size |
198
+ | `--stuic-slider-thumb-inset` | `3px` | Gap between thumb and track edge |
199
+ | `--stuic-slider-radius` | `9999px` | Track corner radius |
200
+ | `--stuic-slider-fill-radius` | `--stuic-slider-radius` | Fill radius (when `fillRounded`) |
201
+ | `--stuic-slider-thumb-radius` | `9999px` | Thumb corner radius |
202
+ | `--stuic-slider-thumb-shadow` | `--stuic-shadow` | Thumb shadow |
203
+ | `--stuic-slider-tick-size` | `4px` | Tick mark diameter |
204
+ | `--stuic-slider-value-gap` | `0.375rem` | Gap between track and value label |
205
+ | `--stuic-slider-transition` | `--stuic-transition` | Fill/thumb movement transition |