@nanahoshi/mona-ui 0.11.4 → 0.13.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.
@@ -0,0 +1,606 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, TemplateRef, Directive, computed, viewChild, contentChild, signal, input, output, model, Component } from '@angular/core';
4
+ import { LucideStar, LucideHeart, LucideFlame, LucideDiamond, LucideCircle, LucideDynamicIcon } from '@lucide/angular';
5
+ import { createElementControlId } from '@nanahoshi/mona-ui/internal';
6
+ import { range } from '@mirei/ts-collections';
7
+ import { twMerge } from 'tailwind-merge';
8
+ import { cva } from 'class-variance-authority';
9
+
10
+ /**
11
+ * @description Renders the pointer-preview visual for rating items. Overrides the default preset
12
+ * overlay whenever the visible state comes from pointer preview.
13
+ */
14
+ class RatingHoveredItemTemplateDirective {
15
+ constructor() {
16
+ this.templateRef = inject(TemplateRef);
17
+ }
18
+ static ngTemplateContextGuard(_directive, context) {
19
+ return true;
20
+ }
21
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingHoveredItemTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
22
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: RatingHoveredItemTemplateDirective, isStandalone: true, selector: "ng-template[monaRatingHoveredItemTemplate]", ngImport: i0 }); }
23
+ }
24
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingHoveredItemTemplateDirective, decorators: [{
25
+ type: Directive,
26
+ args: [{
27
+ selector: "ng-template[monaRatingHoveredItemTemplate]"
28
+ }]
29
+ }] });
30
+
31
+ /**
32
+ * @description Renders the default (unselected) visual for every rating item. Overrides the
33
+ * default preset icon for the base layer of each item.
34
+ */
35
+ class RatingItemTemplateDirective {
36
+ constructor() {
37
+ this.templateRef = inject(TemplateRef);
38
+ }
39
+ static ngTemplateContextGuard(_directive, context) {
40
+ return true;
41
+ }
42
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingItemTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
43
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: RatingItemTemplateDirective, isStandalone: true, selector: "ng-template[monaRatingItemTemplate]", ngImport: i0 }); }
44
+ }
45
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingItemTemplateDirective, decorators: [{
46
+ type: Directive,
47
+ args: [{
48
+ selector: "ng-template[monaRatingItemTemplate]"
49
+ }]
50
+ }] });
51
+
52
+ /**
53
+ * @description Renders the committed selected visual for rating items. Overrides the default
54
+ * preset overlay whenever the visible state comes from the committed value.
55
+ */
56
+ class RatingSelectedItemTemplateDirective {
57
+ constructor() {
58
+ this.templateRef = inject(TemplateRef);
59
+ }
60
+ static ngTemplateContextGuard(_directive, context) {
61
+ return true;
62
+ }
63
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingSelectedItemTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
64
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: RatingSelectedItemTemplateDirective, isStandalone: true, selector: "ng-template[monaRatingSelectedItemTemplate]", ngImport: i0 }); }
65
+ }
66
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingSelectedItemTemplateDirective, decorators: [{
67
+ type: Directive,
68
+ args: [{
69
+ selector: "ng-template[monaRatingSelectedItemTemplate]"
70
+ }]
71
+ }] });
72
+
73
+ const ratingContainerThemeVariants = cva(`inline-flex items-center gap-2.5`);
74
+ const ratingControlThemeVariants = cva(`
75
+ group
76
+ inline-flex items-center
77
+ cursor-pointer select-none
78
+ rounded-md
79
+ outline-none
80
+ transition-colors duration-(--mona-motion-fast)
81
+ focus-visible:ring-2 focus-visible:ring-focus-indicator/35
82
+ data-[disabled="true"]:pointer-events-none
83
+ data-[disabled="true"]:cursor-not-allowed
84
+ data-[readonly="true"]:cursor-default
85
+ data-[invalid="true"]:ring-2 data-[invalid="true"]:ring-error/35
86
+ data-[invalid="true"]:focus-visible:ring-error/35
87
+ `, {
88
+ variants: {
89
+ size: {
90
+ small: "gap-1 p-0.5",
91
+ medium: "gap-1 p-1",
92
+ large: "gap-1.5 p-1.5"
93
+ }
94
+ },
95
+ defaultVariants: {
96
+ size: "medium"
97
+ }
98
+ });
99
+ const ratingItemThemeVariants = cva(`relative flex items-center justify-center overflow-hidden`, {
100
+ variants: {
101
+ size: {
102
+ small: "h-6 w-6",
103
+ medium: "h-7 w-7",
104
+ large: "h-8 w-8"
105
+ }
106
+ },
107
+ defaultVariants: {
108
+ size: "medium"
109
+ }
110
+ });
111
+ const ratingIconThemeVariants = cva(`
112
+ block shrink-0
113
+ text-muted-foreground opacity-45
114
+ transition-colors duration-(--mona-motion-fast)
115
+ group-data-[disabled="true"]:text-disabled-foreground
116
+ `, {
117
+ variants: {
118
+ size: {
119
+ small: "h-4 w-4",
120
+ medium: "h-5 w-5",
121
+ large: "h-6 w-6"
122
+ }
123
+ },
124
+ defaultVariants: {
125
+ size: "medium"
126
+ }
127
+ });
128
+ const ratingOverlayIconThemeVariants = cva(`block shrink-0`, {
129
+ variants: {
130
+ size: {
131
+ small: "h-4 w-4",
132
+ medium: "h-5 w-5",
133
+ large: "h-6 w-6"
134
+ }
135
+ },
136
+ defaultVariants: {
137
+ size: "medium"
138
+ }
139
+ });
140
+ const ratingOverlayClipThemeVariants = cva(`
141
+ absolute bottom-0 start-0 top-0
142
+ pointer-events-none
143
+ overflow-hidden
144
+ `);
145
+ const ratingOverlayContentThemeVariants = cva(`
146
+ flex items-center justify-center
147
+ transition-colors duration-(--mona-motion-fast)
148
+ group-data-[disabled="true"]:text-disabled-foreground
149
+ `, {
150
+ variants: {
151
+ size: {
152
+ small: "h-6 w-6",
153
+ medium: "h-7 w-7",
154
+ large: "h-8 w-8"
155
+ }
156
+ },
157
+ defaultVariants: {
158
+ size: "medium"
159
+ }
160
+ });
161
+ const ratingLabelThemeVariants = cva(`whitespace-nowrap`, {
162
+ variants: {
163
+ size: {
164
+ small: "text-sm",
165
+ medium: "text-sm",
166
+ large: "text-base"
167
+ }
168
+ },
169
+ defaultVariants: {
170
+ size: "medium"
171
+ }
172
+ });
173
+
174
+ const DEFAULT_ITEMS_COUNT = 5;
175
+ /**
176
+ * Normalizes an arbitrary `itemsCount` value into a safe positive integer.
177
+ * Numeric strings are converted, fractional values are floored, and
178
+ * non-finite values fall back to the default.
179
+ */
180
+ function normalizeItemsCount(value) {
181
+ if (typeof value === "string") {
182
+ if (value.trim() === "") {
183
+ return DEFAULT_ITEMS_COUNT;
184
+ }
185
+ value = Number(value);
186
+ }
187
+ if (typeof value !== "number" || !Number.isFinite(value)) {
188
+ return DEFAULT_ITEMS_COUNT;
189
+ }
190
+ return Math.max(1, Math.floor(value));
191
+ }
192
+ /**
193
+ * Returns the interaction step for the given precision.
194
+ */
195
+ function getRatingStep(precision) {
196
+ return precision === "half" ? 0.5 : 1;
197
+ }
198
+ /**
199
+ * Rounds a value to the nearest multiple of the given step while avoiding
200
+ * floating-point drift. The result is constrained to one decimal place.
201
+ */
202
+ function normalizeToStep(value, step) {
203
+ const normalized = Math.round((value + Number.EPSILON) / step) * step;
204
+ return Number(normalized.toFixed(1));
205
+ }
206
+ /**
207
+ * Normalizes an externally supplied value for rendering and interaction.
208
+ * Non-finite values become `0`, out-of-range values are clamped to the
209
+ * valid domain, and the result is snapped to the active precision step.
210
+ */
211
+ function normalizeRatingValue(value, itemsCount, precision) {
212
+ if (typeof value !== "number" || !Number.isFinite(value)) {
213
+ return 0;
214
+ }
215
+ const clamped = Math.max(0, Math.min(itemsCount, value));
216
+ return Math.max(0, Math.min(itemsCount, normalizeToStep(clamped, getRatingStep(precision))));
217
+ }
218
+ /**
219
+ * Fill amount for one item in continuous selection mode: every item at or
220
+ * below the display value fills fully, one boundary item fills partially.
221
+ */
222
+ function getContinuousItemFill(displayValue, itemValue) {
223
+ return Math.max(0, Math.min(1, displayValue - (itemValue - 1)));
224
+ }
225
+ /**
226
+ * Fill amount for one item in single selection mode: only the item
227
+ * representing the display value fills, possibly partially.
228
+ */
229
+ function getSingleItemFill(displayValue, itemValue) {
230
+ if (displayValue === 0) {
231
+ return 0;
232
+ }
233
+ const activeItem = Math.ceil(displayValue);
234
+ if (itemValue !== activeItem) {
235
+ return 0;
236
+ }
237
+ return Math.max(0, Math.min(1, displayValue - (itemValue - 1)));
238
+ }
239
+ /**
240
+ * Converts a pointer position inside an item into a candidate rating value.
241
+ * Whole-item precision always selects the full item; half precision splits
242
+ * the item into two horizontal regions, honoring the effective text
243
+ * direction so the first visual half maps to the lower value.
244
+ */
245
+ function getPointerRatingValue(args) {
246
+ const { clientX, direction, itemRect, itemValue, precision } = args;
247
+ if (precision === "item") {
248
+ return itemValue;
249
+ }
250
+ let ratio = (clientX - itemRect.left) / itemRect.width;
251
+ if (direction === "rtl") {
252
+ ratio = 1 - ratio;
253
+ }
254
+ return ratio <= 0.5 ? itemValue - 0.5 : itemValue;
255
+ }
256
+
257
+ const ratingIcons = {
258
+ circle: LucideCircle,
259
+ diamond: LucideDiamond,
260
+ flame: LucideFlame,
261
+ heart: LucideHeart,
262
+ star: LucideStar
263
+ };
264
+ class RatingComponent {
265
+ constructor() {
266
+ this.activeState = computed(() => this.previewActive() ? "hovered" : "selected", /* @ts-ignore */
267
+ ...(ngDevMode ? [{ debugName: "activeState" }] : /* istanbul ignore next */ []));
268
+ this.containerClasses = computed(() => twMerge(ratingContainerThemeVariants(), this.userClass()), /* @ts-ignore */
269
+ ...(ngDevMode ? [{ debugName: "containerClasses" }] : /* istanbul ignore next */ []));
270
+ this.controlClasses = computed(() => ratingControlThemeVariants({ size: this.size() }), /* @ts-ignore */
271
+ ...(ngDevMode ? [{ debugName: "controlClasses" }] : /* istanbul ignore next */ []));
272
+ this.controlElement = viewChild.required("ratingControl", /* @ts-ignore */
273
+ ...(ngDevMode ? [{ debugName: "controlElement" }] : /* istanbul ignore next */ []));
274
+ this.displayValue = computed(() => this.previewValue() ?? this.normalizedValue(), /* @ts-ignore */
275
+ ...(ngDevMode ? [{ debugName: "displayValue" }] : /* istanbul ignore next */ []));
276
+ this.effectiveAriaLabelledBy = computed(() => {
277
+ if (this.ariaLabelledBy()) {
278
+ return this.ariaLabelledBy();
279
+ }
280
+ if (!this.ariaLabel() && this.label()) {
281
+ return this.labelId;
282
+ }
283
+ return null;
284
+ }, /* @ts-ignore */
285
+ ...(ngDevMode ? [{ debugName: "effectiveAriaLabelledBy" }] : /* istanbul ignore next */ []));
286
+ this.effectiveAriaValueText = computed(() => {
287
+ const custom = this.ariaValueText();
288
+ if (custom) {
289
+ return custom(this.normalizedValue(), this.itemsCount());
290
+ }
291
+ return this.normalizedValue() === 0 ? "Not rated" : `${this.normalizedValue()} out of ${this.itemsCount()}`;
292
+ }, /* @ts-ignore */
293
+ ...(ngDevMode ? [{ debugName: "effectiveAriaValueText" }] : /* istanbul ignore next */ []));
294
+ this.effectiveTabIndex = computed(() => (this.disabled() ? -1 : this.tabindex()), /* @ts-ignore */
295
+ ...(ngDevMode ? [{ debugName: "effectiveTabIndex" }] : /* istanbul ignore next */ []));
296
+ this.hoveredTemplate = contentChild(RatingHoveredItemTemplateDirective, { ...(ngDevMode ? { debugName: "hoveredTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
297
+ this.iconClasses = computed(() => ratingIconThemeVariants({ size: this.size() }), /* @ts-ignore */
298
+ ...(ngDevMode ? [{ debugName: "iconClasses" }] : /* istanbul ignore next */ []));
299
+ this.iconToRender = computed(() => ratingIcons[this.icon()], /* @ts-ignore */
300
+ ...(ngDevMode ? [{ debugName: "iconToRender" }] : /* istanbul ignore next */ []));
301
+ this.interactionDisabled = computed(() => this.disabled() || this.readonly(), /* @ts-ignore */
302
+ ...(ngDevMode ? [{ debugName: "interactionDisabled" }] : /* istanbul ignore next */ []));
303
+ this.interactionStep = computed(() => getRatingStep(this.precision()), /* @ts-ignore */
304
+ ...(ngDevMode ? [{ debugName: "interactionStep" }] : /* istanbul ignore next */ []));
305
+ this.invalidState = computed(() => this.touched() && this.invalid(), /* @ts-ignore */
306
+ ...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
307
+ this.itemClasses = computed(() => ratingItemThemeVariants({ size: this.size() }), /* @ts-ignore */
308
+ ...(ngDevMode ? [{ debugName: "itemClasses" }] : /* istanbul ignore next */ []));
309
+ this.itemTemplate = contentChild(RatingItemTemplateDirective, { ...(ngDevMode ? { debugName: "itemTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
310
+ this.items = computed(() => range(1, this.itemsCount())
311
+ .select(itemValue => ({ index: itemValue - 1, itemValue }))
312
+ .toArray(), /* @ts-ignore */
313
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
314
+ this.labelClasses = computed(() => ratingLabelThemeVariants({ size: this.size() }), /* @ts-ignore */
315
+ ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
316
+ this.labelId = createElementControlId();
317
+ this.normalizedValue = computed(() => normalizeRatingValue(this.value(), this.itemsCount(), this.precision()), /* @ts-ignore */
318
+ ...(ngDevMode ? [{ debugName: "normalizedValue" }] : /* istanbul ignore next */ []));
319
+ this.overlayClipClasses = computed(() => ratingOverlayClipThemeVariants(), /* @ts-ignore */
320
+ ...(ngDevMode ? [{ debugName: "overlayClipClasses" }] : /* istanbul ignore next */ []));
321
+ this.overlayColor = computed(() => this.color() || "var(--color-primary)", /* @ts-ignore */
322
+ ...(ngDevMode ? [{ debugName: "overlayColor" }] : /* istanbul ignore next */ []));
323
+ this.overlayContentClasses = computed(() => ratingOverlayContentThemeVariants({ size: this.size() }), /* @ts-ignore */
324
+ ...(ngDevMode ? [{ debugName: "overlayContentClasses" }] : /* istanbul ignore next */ []));
325
+ this.overlayIconClasses = computed(() => ratingOverlayIconThemeVariants({ size: this.size() }), /* @ts-ignore */
326
+ ...(ngDevMode ? [{ debugName: "overlayIconClasses" }] : /* istanbul ignore next */ []));
327
+ this.overlayTemplate = computed(() => this.activeState() === "hovered" ? this.hoveredTemplate() : this.selectedTemplate(), /* @ts-ignore */
328
+ ...(ngDevMode ? [{ debugName: "overlayTemplate" }] : /* istanbul ignore next */ []));
329
+ this.previewActive = computed(() => this.previewValue() !== null, /* @ts-ignore */
330
+ ...(ngDevMode ? [{ debugName: "previewActive" }] : /* istanbul ignore next */ []));
331
+ this.previewValue = signal(null, /* @ts-ignore */
332
+ ...(ngDevMode ? [{ debugName: "previewValue" }] : /* istanbul ignore next */ []));
333
+ this.selectedTemplate = contentChild(RatingSelectedItemTemplateDirective, { ...(ngDevMode ? { debugName: "selectedTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
334
+ /**
335
+ * @description Associates help text or an error description with the focusable rating control.
336
+ * @default null
337
+ */
338
+ this.ariaDescribedBy = input(null, { ...(ngDevMode ? { debugName: "ariaDescribedBy" } : /* istanbul ignore next */ {}), alias: "aria-describedby" });
339
+ /**
340
+ * @description Explicit accessible name for the rating control. Provide `aria-label`,
341
+ * `aria-labelledby`, or `label` so assistive technology can announce the control.
342
+ * @default null
343
+ */
344
+ this.ariaLabel = input(null, { ...(ngDevMode ? { debugName: "ariaLabel" } : /* istanbul ignore next */ {}), alias: "aria-label" });
345
+ /**
346
+ * @description IDs of external elements providing the accessible name for the rating control.
347
+ * @default null
348
+ */
349
+ this.ariaLabelledBy = input(null, { ...(ngDevMode ? { debugName: "ariaLabelledBy" } : /* istanbul ignore next */ {}), alias: "aria-labelledby" });
350
+ /**
351
+ * @description Custom screen-reader value announcement. Receives the normalized value and the
352
+ * item count and returns the string to expose through `aria-valuetext`.
353
+ * @default null
354
+ */
355
+ this.ariaValueText = input(null, /* @ts-ignore */
356
+ ...(ngDevMode ? [{ debugName: "ariaValueText" }] : /* istanbul ignore next */ []));
357
+ /**
358
+ * @description CSS color for selected and hovered rating visuals. Empty, `null`, and `undefined` values use the
359
+ * primary theme color.
360
+ * @default undefined
361
+ */
362
+ this.color = input(/* @ts-ignore */
363
+ ...(ngDevMode ? [undefined, { debugName: "color" }] : /* istanbul ignore next */ []));
364
+ /**
365
+ * @description Disables pointer and keyboard interaction and removes the control from the tab
366
+ * sequence. When bound to a signal form field via `[formField]`, this is written by the signal
367
+ * forms `Field` directive.
368
+ * @default false
369
+ */
370
+ this.disabled = input(false, /* @ts-ignore */
371
+ ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
372
+ /**
373
+ * @description Built-in icon used for the default rating visuals. Choose `star`, `heart`,
374
+ * `circle`, `diamond`, or `flame`; use item templates to supply a custom icon.
375
+ * @default "star"
376
+ */
377
+ this.icon = input("star", /* @ts-ignore */
378
+ ...(ngDevMode ? [{ debugName: "icon" }] : /* istanbul ignore next */ []));
379
+ /**
380
+ * @description Marks the component as invalid. Error styling requires both `invalid` and
381
+ * `touched` to be `true`. When bound to a signal form field via `[formField]`, this is written
382
+ * by the signal forms `Field` directive.
383
+ * @default false
384
+ */
385
+ this.invalid = input(false, /* @ts-ignore */
386
+ ...(ngDevMode ? [{ debugName: "invalid" }] : /* istanbul ignore next */ []));
387
+ /**
388
+ * @description Number of rating items. Fractional values are floored and values below one are
389
+ * raised to one.
390
+ * @default 5
391
+ */
392
+ this.itemsCount = input(5, { ...(ngDevMode ? { debugName: "itemsCount" } : /* istanbul ignore next */ {}), transform: normalizeItemsCount });
393
+ /**
394
+ * @description Optional visible label rendered before or after the rating items. When no
395
+ * explicit `aria-label` or `aria-labelledby` is supplied, the label provides the accessible
396
+ * name.
397
+ * @default null
398
+ */
399
+ this.label = input(null, /* @ts-ignore */
400
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
401
+ /**
402
+ * @description Places the visible label before or after the item group.
403
+ * @default "after"
404
+ */
405
+ this.labelPosition = input("after", /* @ts-ignore */
406
+ ...(ngDevMode ? [{ debugName: "labelPosition" }] : /* istanbul ignore next */ []));
407
+ /**
408
+ * @description Selection granularity of the rating.
409
+ * @default "item"
410
+ */
411
+ this.precision = input("item", /* @ts-ignore */
412
+ ...(ngDevMode ? [{ debugName: "precision" }] : /* istanbul ignore next */ []));
413
+ /**
414
+ * @description Prevents value changes while preserving focusability and normal visual emphasis.
415
+ * @default false
416
+ */
417
+ this.readonly = input(false, /* @ts-ignore */
418
+ ...(ngDevMode ? [{ debugName: "readonly" }] : /* istanbul ignore next */ []));
419
+ /**
420
+ * @description Controls cumulative or single-item filling.
421
+ * @default "continuous"
422
+ */
423
+ this.selection = input("continuous", /* @ts-ignore */
424
+ ...(ngDevMode ? [{ debugName: "selection" }] : /* istanbul ignore next */ []));
425
+ /**
426
+ * @description Size preset controlling the icon dimensions, item hitboxes, spacing, and label
427
+ * typography.
428
+ * @default "medium"
429
+ */
430
+ this.size = input("medium", /* @ts-ignore */
431
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
432
+ /**
433
+ * @description Tab index applied to the focusable rating control when not disabled. Accepts a
434
+ * numeric string and converts it to a number.
435
+ * @default 0
436
+ */
437
+ this.tabindex = input(0, { ...(ngDevMode ? { debugName: "tabindex" } : /* istanbul ignore next */ {}), transform: (value) => (typeof value === "string" ? parseInt(value, 10) : value) });
438
+ /**
439
+ * @description Emitted when the user interacts with the control, marking the field as touched.
440
+ */
441
+ this.touch = output();
442
+ /**
443
+ * @description Marks the component as touched. When bound to a signal form field via
444
+ * `[formField]`, this is written by the signal forms `Field` directive.
445
+ * @default false
446
+ */
447
+ this.touched = input(false, /* @ts-ignore */
448
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
449
+ /**
450
+ * @description Additional CSS classes merged onto the host element via `tailwind-merge`.
451
+ * @default ""
452
+ */
453
+ this.userClass = input("", { ...(ngDevMode ? { debugName: "userClass" } : /* istanbul ignore next */ {}), alias: "class" });
454
+ /**
455
+ * @description Current rating value. `0` represents an unrated state. Values are normalized
456
+ * for rendering but are not rewritten until the user interacts with the component.
457
+ * @default 0
458
+ */
459
+ this.value = model(0, /* @ts-ignore */
460
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
461
+ }
462
+ blur() {
463
+ this.controlElement().nativeElement.blur();
464
+ }
465
+ focus(options) {
466
+ if (!this.disabled()) {
467
+ this.controlElement().nativeElement.focus(options);
468
+ }
469
+ }
470
+ createItemContext(item) {
471
+ return {
472
+ $implicit: item.index,
473
+ fill: this.getItemFill(item.itemValue),
474
+ hovered: this.previewActive(),
475
+ index: item.index,
476
+ itemValue: item.itemValue,
477
+ selected: !this.previewActive()
478
+ };
479
+ }
480
+ getItemFill(itemValue) {
481
+ const displayValue = this.displayValue();
482
+ if (this.selection() === "single") {
483
+ return getSingleItemFill(displayValue, itemValue);
484
+ }
485
+ return getContinuousItemFill(displayValue, itemValue);
486
+ }
487
+ onBlur() {
488
+ this.previewValue.set(null);
489
+ this.touch.emit();
490
+ }
491
+ onClick(event) {
492
+ if (this.interactionDisabled()) {
493
+ return;
494
+ }
495
+ const candidate = this.getCandidateFromPointerEvent(event);
496
+ if (candidate === null) {
497
+ return;
498
+ }
499
+ const normalized = this.normalizeCandidate(candidate);
500
+ if (normalized !== this.normalizedValue()) {
501
+ this.value.set(normalized);
502
+ }
503
+ this.touch.emit();
504
+ }
505
+ onKeydown(event) {
506
+ if (this.interactionDisabled()) {
507
+ return;
508
+ }
509
+ const action = this.getKeyAction(event.key);
510
+ if (action === null) {
511
+ return;
512
+ }
513
+ event.preventDefault();
514
+ this.previewValue.set(null);
515
+ const current = this.normalizedValue();
516
+ let next;
517
+ switch (action) {
518
+ case "decrease":
519
+ next = this.normalizeCandidate(current - this.interactionStep());
520
+ break;
521
+ case "end":
522
+ next = this.itemsCount();
523
+ break;
524
+ case "home":
525
+ next = 0;
526
+ break;
527
+ case "increase":
528
+ next = this.normalizeCandidate(current + this.interactionStep());
529
+ break;
530
+ }
531
+ if (next !== current) {
532
+ this.value.set(next);
533
+ }
534
+ this.touch.emit();
535
+ }
536
+ onPointerLeave() {
537
+ this.previewValue.set(null);
538
+ }
539
+ onPointerMove(event) {
540
+ if (this.interactionDisabled()) {
541
+ return;
542
+ }
543
+ const candidate = this.getCandidateFromPointerEvent(event);
544
+ if (candidate !== null) {
545
+ this.previewValue.set(candidate);
546
+ }
547
+ }
548
+ getCandidateFromPointerEvent(event) {
549
+ const controlElement = this.controlElement().nativeElement;
550
+ const target = event.target;
551
+ const itemElement = target?.closest("[data-rating-index]") ?? null;
552
+ if (!itemElement || !controlElement.contains(itemElement)) {
553
+ return null;
554
+ }
555
+ const itemValue = Number(itemElement.getAttribute("data-rating-value"));
556
+ const itemRect = itemElement.getBoundingClientRect();
557
+ const direction = getComputedStyle(controlElement).direction === "rtl" ? "rtl" : "ltr";
558
+ return getPointerRatingValue({
559
+ clientX: event.clientX,
560
+ direction,
561
+ itemRect,
562
+ itemValue,
563
+ precision: this.precision()
564
+ });
565
+ }
566
+ getKeyAction(key) {
567
+ switch (key) {
568
+ case "ArrowRight":
569
+ case "ArrowUp":
570
+ return "increase";
571
+ case "ArrowLeft":
572
+ case "ArrowDown":
573
+ return "decrease";
574
+ case "Home":
575
+ return "home";
576
+ case "End":
577
+ return "end";
578
+ default:
579
+ return null;
580
+ }
581
+ }
582
+ normalizeCandidate(value) {
583
+ return normalizeRatingValue(value, this.itemsCount(), this.precision());
584
+ }
585
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
586
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: RatingComponent, isStandalone: true, selector: "mona-rating", inputs: { ariaDescribedBy: { classPropertyName: "ariaDescribedBy", publicName: "aria-describedby", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, ariaValueText: { classPropertyName: "ariaValueText", publicName: "ariaValueText", isSignal: true, isRequired: false, transformFunction: null }, color: { classPropertyName: "color", publicName: "color", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, itemsCount: { classPropertyName: "itemsCount", publicName: "itemsCount", isSignal: true, isRequired: false, transformFunction: null }, label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: false, transformFunction: null }, labelPosition: { classPropertyName: "labelPosition", publicName: "labelPosition", isSignal: true, isRequired: false, transformFunction: null }, precision: { classPropertyName: "precision", publicName: "precision", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, tabindex: { classPropertyName: "tabindex", publicName: "tabindex", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touch: "touch", value: "valueChange" }, queries: [{ propertyName: "hoveredTemplate", first: true, predicate: RatingHoveredItemTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "itemTemplate", first: true, predicate: RatingItemTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }, { propertyName: "selectedTemplate", first: true, predicate: RatingSelectedItemTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], viewQueries: [{ propertyName: "controlElement", first: true, predicate: ["ratingControl"], descendants: true, isSignal: true }], ngImport: i0, template: "<div [class]=\"containerClasses()\">\n @if (label() && labelPosition() === \"before\") {\n <span [id]=\"labelId\" [class]=\"labelClasses()\">{{ label() }}</span>\n }\n\n <div\n #ratingControl\n role=\"slider\"\n [class]=\"controlClasses()\"\n [attr.tabindex]=\"effectiveTabIndex()\"\n [attr.aria-valuemin]=\"0\"\n [attr.aria-valuemax]=\"itemsCount()\"\n [attr.aria-valuenow]=\"normalizedValue()\"\n [attr.aria-valuetext]=\"effectiveAriaValueText()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"effectiveAriaLabelledBy()\"\n [attr.aria-describedby]=\"ariaDescribedBy()\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-invalid]=\"invalidState() || null\"\n [attr.data-disabled]=\"disabled() || null\"\n [attr.data-readonly]=\"readonly() || null\"\n [attr.data-invalid]=\"invalidState() || null\"\n [attr.data-precision]=\"precision()\"\n [attr.data-selection]=\"selection()\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n (click)=\"onClick($event)\">\n @for (item of items(); track item.itemValue) {\n <span\n [class]=\"itemClasses()\"\n [attr.data-rating-index]=\"item.index\"\n [attr.data-rating-value]=\"item.itemValue\"\n aria-hidden=\"true\">\n <span class=\"flex h-full w-full items-center justify-center\">\n @if (itemTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"itemTemplate()!\"\n [ngTemplateOutletContext]=\"createItemContext(item)\">\n </ng-container>\n } @else {\n <svg lucideIcon [lucideIcon]=\"iconToRender()\" [class]=\"iconClasses()\" fill=\"none\"></svg>\n }\n </span>\n\n @let fill = getItemFill(item.itemValue);\n\n @if (fill > 0) {\n <span [class]=\"overlayClipClasses()\" [style.inline-size.%]=\"fill * 100\">\n <span\n [class]=\"overlayContentClasses()\"\n [attr.data-state]=\"activeState()\"\n [style.color]=\"disabled() ? null : overlayColor()\">\n @if (overlayTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"overlayTemplate()!\"\n [ngTemplateOutletContext]=\"createItemContext(item)\">\n </ng-container>\n } @else {\n <svg\n lucideIcon\n [lucideIcon]=\"iconToRender()\"\n [class]=\"overlayIconClasses()\"\n fill=\"currentColor\"></svg>\n }\n </span>\n </span>\n }\n </span>\n }\n </div>\n\n @if (label() && labelPosition() === \"after\") {\n <span [id]=\"labelId\" [class]=\"labelClasses()\">{{ label() }}</span>\n }\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }] }); }
587
+ }
588
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: RatingComponent, decorators: [{
589
+ type: Component,
590
+ args: [{ selector: "mona-rating", imports: [NgTemplateOutlet, LucideDynamicIcon], template: "<div [class]=\"containerClasses()\">\n @if (label() && labelPosition() === \"before\") {\n <span [id]=\"labelId\" [class]=\"labelClasses()\">{{ label() }}</span>\n }\n\n <div\n #ratingControl\n role=\"slider\"\n [class]=\"controlClasses()\"\n [attr.tabindex]=\"effectiveTabIndex()\"\n [attr.aria-valuemin]=\"0\"\n [attr.aria-valuemax]=\"itemsCount()\"\n [attr.aria-valuenow]=\"normalizedValue()\"\n [attr.aria-valuetext]=\"effectiveAriaValueText()\"\n [attr.aria-label]=\"ariaLabel()\"\n [attr.aria-labelledby]=\"effectiveAriaLabelledBy()\"\n [attr.aria-describedby]=\"ariaDescribedBy()\"\n [attr.aria-disabled]=\"disabled() || null\"\n [attr.aria-readonly]=\"readonly() || null\"\n [attr.aria-invalid]=\"invalidState() || null\"\n [attr.data-disabled]=\"disabled() || null\"\n [attr.data-readonly]=\"readonly() || null\"\n [attr.data-invalid]=\"invalidState() || null\"\n [attr.data-precision]=\"precision()\"\n [attr.data-selection]=\"selection()\"\n (keydown)=\"onKeydown($event)\"\n (blur)=\"onBlur()\"\n (pointermove)=\"onPointerMove($event)\"\n (pointerleave)=\"onPointerLeave()\"\n (click)=\"onClick($event)\">\n @for (item of items(); track item.itemValue) {\n <span\n [class]=\"itemClasses()\"\n [attr.data-rating-index]=\"item.index\"\n [attr.data-rating-value]=\"item.itemValue\"\n aria-hidden=\"true\">\n <span class=\"flex h-full w-full items-center justify-center\">\n @if (itemTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"itemTemplate()!\"\n [ngTemplateOutletContext]=\"createItemContext(item)\">\n </ng-container>\n } @else {\n <svg lucideIcon [lucideIcon]=\"iconToRender()\" [class]=\"iconClasses()\" fill=\"none\"></svg>\n }\n </span>\n\n @let fill = getItemFill(item.itemValue);\n\n @if (fill > 0) {\n <span [class]=\"overlayClipClasses()\" [style.inline-size.%]=\"fill * 100\">\n <span\n [class]=\"overlayContentClasses()\"\n [attr.data-state]=\"activeState()\"\n [style.color]=\"disabled() ? null : overlayColor()\">\n @if (overlayTemplate()) {\n <ng-container\n [ngTemplateOutlet]=\"overlayTemplate()!\"\n [ngTemplateOutletContext]=\"createItemContext(item)\">\n </ng-container>\n } @else {\n <svg\n lucideIcon\n [lucideIcon]=\"iconToRender()\"\n [class]=\"overlayIconClasses()\"\n fill=\"currentColor\"></svg>\n }\n </span>\n </span>\n }\n </span>\n }\n </div>\n\n @if (label() && labelPosition() === \"after\") {\n <span [id]=\"labelId\" [class]=\"labelClasses()\">{{ label() }}</span>\n }\n</div>\n" }]
591
+ }], propDecorators: { controlElement: [{ type: i0.ViewChild, args: ["ratingControl", { isSignal: true }] }], hoveredTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RatingHoveredItemTemplateDirective), { ...{
592
+ read: TemplateRef
593
+ }, isSignal: true }] }], itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RatingItemTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], selectedTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => RatingSelectedItemTemplateDirective), { ...{
594
+ read: TemplateRef
595
+ }, isSignal: true }] }], ariaDescribedBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-describedby", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], ariaValueText: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaValueText", required: false }] }], color: [{ type: i0.Input, args: [{ isSignal: true, alias: "color", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], itemsCount: [{ type: i0.Input, args: [{ isSignal: true, alias: "itemsCount", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], labelPosition: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelPosition", required: false }] }], precision: [{ type: i0.Input, args: [{ isSignal: true, alias: "precision", required: false }] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], tabindex: [{ type: i0.Input, args: [{ isSignal: true, alias: "tabindex", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
596
+
597
+ /*
598
+ * Public API Surface of @nanahoshi/mona-ui/rating
599
+ */
600
+
601
+ /**
602
+ * Generated bundle index. Do not edit.
603
+ */
604
+
605
+ export { RatingComponent, RatingHoveredItemTemplateDirective, RatingItemTemplateDirective, RatingSelectedItemTemplateDirective };
606
+ //# sourceMappingURL=nanahoshi-mona-ui-rating.mjs.map
@@ -1,9 +1,31 @@
1
+ import { NgTemplateOutlet } from '@angular/common';
1
2
  import * as i0 from '@angular/core';
2
- import { computed, input, output, model, Component } from '@angular/core';
3
+ import { inject, TemplateRef, Directive, computed, contentChild, input, output, model, Component } from '@angular/core';
3
4
  import { themeControlSurfaceClasses, createElementControlId } from '@nanahoshi/mona-ui/internal';
4
5
  import { twMerge } from 'tailwind-merge';
5
6
  import { cva } from 'class-variance-authority';
6
7
 
8
+ /**
9
+ * @description Renders the visual content of every segmented option. Overrides the default label
10
+ * text of each option.
11
+ */
12
+ class SegmentedItemTemplateDirective {
13
+ constructor() {
14
+ this.templateRef = inject(TemplateRef);
15
+ }
16
+ static ngTemplateContextGuard(_directive, context) {
17
+ return true;
18
+ }
19
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SegmentedItemTemplateDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
20
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.0.6", type: SegmentedItemTemplateDirective, isStandalone: true, selector: "ng-template[monaSegmentedItemTemplate]", ngImport: i0 }); }
21
+ }
22
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SegmentedItemTemplateDirective, decorators: [{
23
+ type: Directive,
24
+ args: [{
25
+ selector: "ng-template[monaSegmentedItemTemplate]"
26
+ }]
27
+ }] });
28
+
7
29
  const segmentedContainerThemeVariants = cva(`
8
30
  inline-flex items-center gap-0.5
9
31
  ${themeControlSurfaceClasses}
@@ -78,6 +100,7 @@ class SegmentedComponent {
78
100
  ...(ngDevMode ? [{ debugName: "inputClasses" }] : /* istanbul ignore next */ []));
79
101
  this.invalidState = computed(() => this.touched() && this.invalid(), /* @ts-ignore */
80
102
  ...(ngDevMode ? [{ debugName: "invalidState" }] : /* istanbul ignore next */ []));
103
+ this.itemTemplate = contentChild(SegmentedItemTemplateDirective, { ...(ngDevMode ? { debugName: "itemTemplate" } : /* istanbul ignore next */ {}), read: TemplateRef });
81
104
  this.optionClasses = computed(() => {
82
105
  const rounded = this.rounded();
83
106
  const size = this.size();
@@ -148,6 +171,15 @@ class SegmentedComponent {
148
171
  this.value = model(null, /* @ts-ignore */
149
172
  ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
150
173
  }
174
+ createOptionContext(option, index) {
175
+ return {
176
+ $implicit: option,
177
+ disabled: this.disabled() || !!option.disabled,
178
+ index,
179
+ option,
180
+ selected: option.value === this.value()
181
+ };
182
+ }
151
183
  onOptionBlur() {
152
184
  this.touch.emit();
153
185
  }
@@ -159,11 +191,11 @@ class SegmentedComponent {
159
191
  this.touch.emit();
160
192
  }
161
193
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SegmentedComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
162
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SegmentedComponent, isStandalone: true, selector: "mona-segmented", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touch: "touch", value: "valueChange" }, host: { properties: { "attr.aria-disabled": "disabled() || undefined", "attr.aria-invalid": "invalidState() ? 'true' : null", "attr.aria-label": "ariaLabel()", "attr.aria-labelledby": "ariaLabelledBy()", "attr.data-disabled": "disabled()", "attr.data-invalid": "invalidState() || null", "attr.role": "'radiogroup'", "class": "containerClasses()" } }, ngImport: i0, template: "@for (option of options(); track option.value) {\n @let selected = option.value === value();\n @let optionDisabled = disabled() || option.disabled;\n\n <label [class]=\"optionClasses()\" [attr.data-selected]=\"selected\" [attr.data-disabled]=\"optionDisabled\">\n <input\n type=\"radio\"\n [name]=\"groupName\"\n [value]=\"option.value\"\n [checked]=\"selected\"\n [disabled]=\"optionDisabled\"\n [class]=\"inputClasses()\"\n (change)=\"onOptionChange(option)\"\n (blur)=\"onOptionBlur()\" />\n\n <span\n class=\"pointer-events-none flex flex-1 items-center justify-center gap-1\n [border-radius:inherit]\n peer-focus-visible:ring-2\n peer-focus-visible:ring-focus-indicator/35\">\n {{ option.label }}\n </span>\n </label>\n}\n" }); }
194
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.6", type: SegmentedComponent, isStandalone: true, selector: "mona-segmented", inputs: { ariaLabel: { classPropertyName: "ariaLabel", publicName: "aria-label", isSignal: true, isRequired: false, transformFunction: null }, ariaLabelledBy: { classPropertyName: "ariaLabelledBy", publicName: "aria-labelledby", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, rounded: { classPropertyName: "rounded", publicName: "rounded", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null }, userClass: { classPropertyName: "userClass", publicName: "class", isSignal: true, isRequired: false, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { touch: "touch", value: "valueChange" }, host: { properties: { "attr.aria-disabled": "disabled() || undefined", "attr.aria-invalid": "invalidState() ? 'true' : null", "attr.aria-label": "ariaLabel()", "attr.aria-labelledby": "ariaLabelledBy()", "attr.data-disabled": "disabled()", "attr.data-invalid": "invalidState() || null", "attr.role": "'radiogroup'", "class": "containerClasses()" } }, queries: [{ propertyName: "itemTemplate", first: true, predicate: SegmentedItemTemplateDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "@for (option of options(); track option.value) {\n @let selected = option.value === value();\n @let optionDisabled = disabled() || option.disabled;\n\n <label [class]=\"optionClasses()\" [attr.data-selected]=\"selected\" [attr.data-disabled]=\"optionDisabled\">\n <input\n type=\"radio\"\n [name]=\"groupName\"\n [value]=\"option.value\"\n [checked]=\"selected\"\n [disabled]=\"optionDisabled\"\n [class]=\"inputClasses()\"\n (change)=\"onOptionChange(option)\"\n (blur)=\"onOptionBlur()\" />\n\n <span\n class=\"pointer-events-none flex flex-1 items-center justify-center gap-1\n [border-radius:inherit]\n peer-focus-visible:ring-2\n peer-focus-visible:ring-focus-indicator/35\">\n @if (itemTemplate(); as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"createOptionContext(option, $index)\"></ng-container>\n } @else {\n {{ option.label }}\n }\n </span>\n </label>\n}\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
163
195
  }
164
196
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImport: i0, type: SegmentedComponent, decorators: [{
165
197
  type: Component,
166
- args: [{ selector: "mona-segmented", host: {
198
+ args: [{ selector: "mona-segmented", imports: [NgTemplateOutlet], host: {
167
199
  "[attr.aria-disabled]": "disabled() || undefined",
168
200
  "[attr.aria-invalid]": "invalidState() ? 'true' : null",
169
201
  "[attr.aria-label]": "ariaLabel()",
@@ -172,8 +204,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
172
204
  "[attr.data-invalid]": "invalidState() || null",
173
205
  "[attr.role]": "'radiogroup'",
174
206
  "[class]": "containerClasses()"
175
- }, template: "@for (option of options(); track option.value) {\n @let selected = option.value === value();\n @let optionDisabled = disabled() || option.disabled;\n\n <label [class]=\"optionClasses()\" [attr.data-selected]=\"selected\" [attr.data-disabled]=\"optionDisabled\">\n <input\n type=\"radio\"\n [name]=\"groupName\"\n [value]=\"option.value\"\n [checked]=\"selected\"\n [disabled]=\"optionDisabled\"\n [class]=\"inputClasses()\"\n (change)=\"onOptionChange(option)\"\n (blur)=\"onOptionBlur()\" />\n\n <span\n class=\"pointer-events-none flex flex-1 items-center justify-center gap-1\n [border-radius:inherit]\n peer-focus-visible:ring-2\n peer-focus-visible:ring-focus-indicator/35\">\n {{ option.label }}\n </span>\n </label>\n}\n" }]
176
- }], propDecorators: { ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
207
+ }, template: "@for (option of options(); track option.value) {\n @let selected = option.value === value();\n @let optionDisabled = disabled() || option.disabled;\n\n <label [class]=\"optionClasses()\" [attr.data-selected]=\"selected\" [attr.data-disabled]=\"optionDisabled\">\n <input\n type=\"radio\"\n [name]=\"groupName\"\n [value]=\"option.value\"\n [checked]=\"selected\"\n [disabled]=\"optionDisabled\"\n [class]=\"inputClasses()\"\n (change)=\"onOptionChange(option)\"\n (blur)=\"onOptionBlur()\" />\n\n <span\n class=\"pointer-events-none flex flex-1 items-center justify-center gap-1\n [border-radius:inherit]\n peer-focus-visible:ring-2\n peer-focus-visible:ring-focus-indicator/35\">\n @if (itemTemplate(); as template) {\n <ng-container\n [ngTemplateOutlet]=\"template\"\n [ngTemplateOutletContext]=\"createOptionContext(option, $index)\"></ng-container>\n } @else {\n {{ option.label }}\n }\n </span>\n </label>\n}\n" }]
208
+ }], propDecorators: { itemTemplate: [{ type: i0.ContentChild, args: [i0.forwardRef(() => SegmentedItemTemplateDirective), { ...{ read: TemplateRef }, isSignal: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-label", required: false }] }], ariaLabelledBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "aria-labelledby", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], rounded: [{ type: i0.Input, args: [{ isSignal: true, alias: "rounded", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], touch: [{ type: i0.Output, args: ["touch"] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }], userClass: [{ type: i0.Input, args: [{ isSignal: true, alias: "class", required: false }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }, { type: i0.Output, args: ["valueChange"] }] } });
177
209
 
178
210
  /*
179
211
  * Public API Surface of @nanahoshi/mona-ui/segmented
@@ -183,5 +215,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.6", ngImpor
183
215
  * Generated bundle index. Do not edit.
184
216
  */
185
217
 
186
- export { SegmentedComponent };
218
+ export { SegmentedComponent, SegmentedItemTemplateDirective };
187
219
  //# sourceMappingURL=nanahoshi-mona-ui-segmented.mjs.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanahoshi/mona-ui",
3
- "version": "0.11.4",
3
+ "version": "0.13.0",
4
4
  "description": "A very light Angular UI framework",
5
5
  "author": "Nanahoshi",
6
6
  "repository": {
@@ -261,6 +261,10 @@
261
261
  "types": "./types/nanahoshi-mona-ui-radio-button.d.ts",
262
262
  "default": "./fesm2022/nanahoshi-mona-ui-radio-button.mjs"
263
263
  },
264
+ "./rating": {
265
+ "types": "./types/nanahoshi-mona-ui-rating.d.ts",
266
+ "default": "./fesm2022/nanahoshi-mona-ui-rating.mjs"
267
+ },
264
268
  "./scroll-view": {
265
269
  "types": "./types/nanahoshi-mona-ui-scroll-view.d.ts",
266
270
  "default": "./fesm2022/nanahoshi-mona-ui-scroll-view.mjs"
@@ -0,0 +1,4 @@
1
+ {
2
+ "module": "../fesm2022/nanahoshi-mona-ui-rating.mjs",
3
+ "typings": "../types/nanahoshi-mona-ui-rating.d.ts"
4
+ }
@@ -0,0 +1,295 @@
1
+ import * as _angular_core from '@angular/core';
2
+ import { ElementRef, TemplateRef } from '@angular/core';
3
+ import { FormValueControl } from '@angular/forms/signals';
4
+ import { LucideCircle, LucideDiamond, LucideFlame, LucideHeart, LucideStar } from '@lucide/angular';
5
+ import * as class_variance_authority_types from 'class-variance-authority/types';
6
+ import { VariantProps } from 'class-variance-authority';
7
+ import { VariantInputs } from '@nanahoshi/mona-ui/internal';
8
+
9
+ /**
10
+ * @description Rendering context passed to the default, selected, and hovered item templates.
11
+ */
12
+ interface RatingItemTemplateContext {
13
+ /**
14
+ * Zero-based item index.
15
+ */
16
+ readonly $implicit: number;
17
+ /**
18
+ * Zero-based item index.
19
+ */
20
+ readonly index: number;
21
+ /**
22
+ * One-based value represented by this item.
23
+ */
24
+ readonly itemValue: number;
25
+ /**
26
+ * Visible fill amount between 0 and 1.
27
+ */
28
+ readonly fill: number;
29
+ /**
30
+ * Whether the visible state comes from the committed value.
31
+ */
32
+ readonly selected: boolean;
33
+ /**
34
+ * Whether the visible state comes from pointer preview.
35
+ */
36
+ readonly hovered: boolean;
37
+ }
38
+
39
+ /**
40
+ * @description Built-in icon names available for the default rating visuals.
41
+ */
42
+ type RatingIconName = "circle" | "diamond" | "flame" | "heart" | "star";
43
+
44
+ /**
45
+ * @description Position of the visible label relative to the rating items.
46
+ * @default "after"
47
+ */
48
+ type RatingLabelPosition = "before" | "after";
49
+
50
+ /**
51
+ * @description Selection granularity of the rating. `"item"` selects whole items, `"half"` selects
52
+ * half-item increments.
53
+ * @default "item"
54
+ */
55
+ type RatingPrecision = "item" | "half";
56
+
57
+ /**
58
+ * @description How the selected value is visualized across the items. `"continuous"` fills every
59
+ * item up to the selected value, `"single"` fills only the item representing the selected value.
60
+ * @default "continuous"
61
+ */
62
+ type RatingSelectionMode = "continuous" | "single";
63
+
64
+ declare const ratingContainerThemeVariants: (props?: class_variance_authority_types.ClassProp | undefined) => string;
65
+ declare const ratingControlThemeVariants: (props?: ({
66
+ size?: "small" | "medium" | "large" | null | undefined;
67
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
68
+ declare const ratingItemThemeVariants: (props?: ({
69
+ size?: "small" | "medium" | "large" | null | undefined;
70
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
71
+ declare const ratingIconThemeVariants: (props?: ({
72
+ size?: "small" | "medium" | "large" | null | undefined;
73
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
74
+ declare const ratingOverlayClipThemeVariants: (props?: class_variance_authority_types.ClassProp | undefined) => string;
75
+ declare const ratingOverlayContentThemeVariants: (props?: ({
76
+ size?: "small" | "medium" | "large" | null | undefined;
77
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
78
+ declare const ratingLabelThemeVariants: (props?: ({
79
+ size?: "small" | "medium" | "large" | null | undefined;
80
+ } & class_variance_authority_types.ClassProp) | undefined) => string;
81
+ type RatingContainerVariantProps = VariantProps<typeof ratingContainerThemeVariants>;
82
+ type RatingContainerVariantInput = VariantInputs<RatingContainerVariantProps>;
83
+ type RatingControlVariantProps = VariantProps<typeof ratingControlThemeVariants>;
84
+ type RatingControlVariantInput = VariantInputs<RatingControlVariantProps>;
85
+ type RatingItemVariantProps = VariantProps<typeof ratingItemThemeVariants>;
86
+ type RatingItemVariantInput = VariantInputs<RatingItemVariantProps>;
87
+ type RatingIconVariantProps = VariantProps<typeof ratingIconThemeVariants>;
88
+ type RatingIconVariantInput = VariantInputs<RatingIconVariantProps>;
89
+ type RatingOverlayClipVariantProps = VariantProps<typeof ratingOverlayClipThemeVariants>;
90
+ type RatingOverlayClipVariantInput = VariantInputs<RatingOverlayClipVariantProps>;
91
+ type RatingOverlayContentVariantProps = VariantProps<typeof ratingOverlayContentThemeVariants>;
92
+ type RatingOverlayContentVariantInput = VariantInputs<RatingOverlayContentVariantProps>;
93
+ type RatingLabelVariantProps = VariantProps<typeof ratingLabelThemeVariants>;
94
+ type RatingLabelVariantInput = VariantInputs<RatingLabelVariantProps>;
95
+ type RatingVariantProps = RatingContainerVariantProps & RatingControlVariantProps & RatingItemVariantProps & RatingIconVariantProps & RatingOverlayClipVariantProps & RatingOverlayContentVariantProps & RatingLabelVariantProps;
96
+ type RatingVariantInput = RatingContainerVariantInput & RatingControlVariantInput & RatingItemVariantInput & RatingIconVariantInput & RatingOverlayClipVariantInput & RatingOverlayContentVariantInput & RatingLabelVariantInput;
97
+
98
+ interface RatingItemDescriptor {
99
+ readonly index: number;
100
+ readonly itemValue: number;
101
+ }
102
+ declare class RatingComponent implements RatingVariantInput, FormValueControl<number> {
103
+ protected readonly activeState: _angular_core.Signal<"hovered" | "selected">;
104
+ protected readonly containerClasses: _angular_core.Signal<string>;
105
+ protected readonly controlClasses: _angular_core.Signal<string>;
106
+ protected readonly controlElement: _angular_core.Signal<ElementRef<HTMLElement>>;
107
+ protected readonly displayValue: _angular_core.Signal<number>;
108
+ protected readonly effectiveAriaLabelledBy: _angular_core.Signal<string | null>;
109
+ protected readonly effectiveAriaValueText: _angular_core.Signal<string>;
110
+ protected readonly effectiveTabIndex: _angular_core.Signal<number>;
111
+ protected readonly hoveredTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
112
+ protected readonly iconClasses: _angular_core.Signal<string>;
113
+ protected readonly iconToRender: _angular_core.Signal<typeof LucideCircle | typeof LucideDiamond | typeof LucideFlame | typeof LucideHeart | typeof LucideStar>;
114
+ protected readonly interactionDisabled: _angular_core.Signal<boolean>;
115
+ protected readonly interactionStep: _angular_core.Signal<number>;
116
+ protected readonly invalidState: _angular_core.Signal<boolean>;
117
+ protected readonly itemClasses: _angular_core.Signal<string>;
118
+ protected readonly itemTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
119
+ protected readonly items: _angular_core.Signal<readonly RatingItemDescriptor[]>;
120
+ protected readonly labelClasses: _angular_core.Signal<string>;
121
+ protected readonly labelId: string;
122
+ protected readonly normalizedValue: _angular_core.Signal<number>;
123
+ protected readonly overlayClipClasses: _angular_core.Signal<string>;
124
+ protected readonly overlayColor: _angular_core.Signal<string>;
125
+ protected readonly overlayContentClasses: _angular_core.Signal<string>;
126
+ protected readonly overlayIconClasses: _angular_core.Signal<string>;
127
+ protected readonly overlayTemplate: _angular_core.Signal<TemplateRef<RatingItemTemplateContext> | undefined>;
128
+ protected readonly previewActive: _angular_core.Signal<boolean>;
129
+ protected readonly previewValue: _angular_core.WritableSignal<number | null>;
130
+ protected readonly selectedTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
131
+ /**
132
+ * @description Associates help text or an error description with the focusable rating control.
133
+ * @default null
134
+ */
135
+ readonly ariaDescribedBy: _angular_core.InputSignal<string | null>;
136
+ /**
137
+ * @description Explicit accessible name for the rating control. Provide `aria-label`,
138
+ * `aria-labelledby`, or `label` so assistive technology can announce the control.
139
+ * @default null
140
+ */
141
+ readonly ariaLabel: _angular_core.InputSignal<string | null>;
142
+ /**
143
+ * @description IDs of external elements providing the accessible name for the rating control.
144
+ * @default null
145
+ */
146
+ readonly ariaLabelledBy: _angular_core.InputSignal<string | null>;
147
+ /**
148
+ * @description Custom screen-reader value announcement. Receives the normalized value and the
149
+ * item count and returns the string to expose through `aria-valuetext`.
150
+ * @default null
151
+ */
152
+ readonly ariaValueText: _angular_core.InputSignal<((value: number, maximum: number) => string) | null>;
153
+ /**
154
+ * @description CSS color for selected and hovered rating visuals. Empty, `null`, and `undefined` values use the
155
+ * primary theme color.
156
+ * @default undefined
157
+ */
158
+ readonly color: _angular_core.InputSignal<string | null | undefined>;
159
+ /**
160
+ * @description Disables pointer and keyboard interaction and removes the control from the tab
161
+ * sequence. When bound to a signal form field via `[formField]`, this is written by the signal
162
+ * forms `Field` directive.
163
+ * @default false
164
+ */
165
+ readonly disabled: _angular_core.InputSignal<boolean>;
166
+ /**
167
+ * @description Built-in icon used for the default rating visuals. Choose `star`, `heart`,
168
+ * `circle`, `diamond`, or `flame`; use item templates to supply a custom icon.
169
+ * @default "star"
170
+ */
171
+ readonly icon: _angular_core.InputSignal<RatingIconName>;
172
+ /**
173
+ * @description Marks the component as invalid. Error styling requires both `invalid` and
174
+ * `touched` to be `true`. When bound to a signal form field via `[formField]`, this is written
175
+ * by the signal forms `Field` directive.
176
+ * @default false
177
+ */
178
+ readonly invalid: _angular_core.InputSignal<boolean>;
179
+ /**
180
+ * @description Number of rating items. Fractional values are floored and values below one are
181
+ * raised to one.
182
+ * @default 5
183
+ */
184
+ readonly itemsCount: _angular_core.InputSignalWithTransform<number, number>;
185
+ /**
186
+ * @description Optional visible label rendered before or after the rating items. When no
187
+ * explicit `aria-label` or `aria-labelledby` is supplied, the label provides the accessible
188
+ * name.
189
+ * @default null
190
+ */
191
+ readonly label: _angular_core.InputSignal<string | null>;
192
+ /**
193
+ * @description Places the visible label before or after the item group.
194
+ * @default "after"
195
+ */
196
+ readonly labelPosition: _angular_core.InputSignal<RatingLabelPosition>;
197
+ /**
198
+ * @description Selection granularity of the rating.
199
+ * @default "item"
200
+ */
201
+ readonly precision: _angular_core.InputSignal<RatingPrecision>;
202
+ /**
203
+ * @description Prevents value changes while preserving focusability and normal visual emphasis.
204
+ * @default false
205
+ */
206
+ readonly readonly: _angular_core.InputSignal<boolean>;
207
+ /**
208
+ * @description Controls cumulative or single-item filling.
209
+ * @default "continuous"
210
+ */
211
+ readonly selection: _angular_core.InputSignal<RatingSelectionMode>;
212
+ /**
213
+ * @description Size preset controlling the icon dimensions, item hitboxes, spacing, and label
214
+ * typography.
215
+ * @default "medium"
216
+ */
217
+ readonly size: _angular_core.InputSignal<"small" | "medium" | "large" | null | undefined>;
218
+ /**
219
+ * @description Tab index applied to the focusable rating control when not disabled. Accepts a
220
+ * numeric string and converts it to a number.
221
+ * @default 0
222
+ */
223
+ readonly tabindex: _angular_core.InputSignalWithTransform<number, string | number>;
224
+ /**
225
+ * @description Emitted when the user interacts with the control, marking the field as touched.
226
+ */
227
+ readonly touch: _angular_core.OutputEmitterRef<void>;
228
+ /**
229
+ * @description Marks the component as touched. When bound to a signal form field via
230
+ * `[formField]`, this is written by the signal forms `Field` directive.
231
+ * @default false
232
+ */
233
+ readonly touched: _angular_core.InputSignal<boolean>;
234
+ /**
235
+ * @description Additional CSS classes merged onto the host element via `tailwind-merge`.
236
+ * @default ""
237
+ */
238
+ readonly userClass: _angular_core.InputSignal<string>;
239
+ /**
240
+ * @description Current rating value. `0` represents an unrated state. Values are normalized
241
+ * for rendering but are not rewritten until the user interacts with the component.
242
+ * @default 0
243
+ */
244
+ readonly value: _angular_core.ModelSignal<number>;
245
+ blur(): void;
246
+ focus(options?: FocusOptions): void;
247
+ protected createItemContext(item: RatingItemDescriptor): RatingItemTemplateContext;
248
+ protected getItemFill(itemValue: number): number;
249
+ protected onBlur(): void;
250
+ protected onClick(event: MouseEvent): void;
251
+ protected onKeydown(event: KeyboardEvent): void;
252
+ protected onPointerLeave(): void;
253
+ protected onPointerMove(event: PointerEvent): void;
254
+ private getCandidateFromPointerEvent;
255
+ private getKeyAction;
256
+ private normalizeCandidate;
257
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RatingComponent, never>;
258
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<RatingComponent, "mona-rating", never, { "ariaDescribedBy": { "alias": "aria-describedby"; "required": false; "isSignal": true; }; "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "ariaLabelledBy": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; "ariaValueText": { "alias": "ariaValueText"; "required": false; "isSignal": true; }; "color": { "alias": "color"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "icon": { "alias": "icon"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "itemsCount": { "alias": "itemsCount"; "required": false; "isSignal": true; }; "label": { "alias": "label"; "required": false; "isSignal": true; }; "labelPosition": { "alias": "labelPosition"; "required": false; "isSignal": true; }; "precision": { "alias": "precision"; "required": false; "isSignal": true; }; "readonly": { "alias": "readonly"; "required": false; "isSignal": true; }; "selection": { "alias": "selection"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "tabindex": { "alias": "tabindex"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "touch": "touch"; "value": "valueChange"; }, ["hoveredTemplate", "itemTemplate", "selectedTemplate"], never, true, never>;
259
+ }
260
+
261
+ /**
262
+ * @description Renders the pointer-preview visual for rating items. Overrides the default preset
263
+ * overlay whenever the visible state comes from pointer preview.
264
+ */
265
+ declare class RatingHoveredItemTemplateDirective {
266
+ readonly templateRef: TemplateRef<RatingItemTemplateContext>;
267
+ static ngTemplateContextGuard(_directive: RatingHoveredItemTemplateDirective, context: unknown): context is RatingItemTemplateContext;
268
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RatingHoveredItemTemplateDirective, never>;
269
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RatingHoveredItemTemplateDirective, "ng-template[monaRatingHoveredItemTemplate]", never, {}, {}, never, never, true, never>;
270
+ }
271
+
272
+ /**
273
+ * @description Renders the default (unselected) visual for every rating item. Overrides the
274
+ * default preset icon for the base layer of each item.
275
+ */
276
+ declare class RatingItemTemplateDirective {
277
+ readonly templateRef: TemplateRef<RatingItemTemplateContext>;
278
+ static ngTemplateContextGuard(_directive: RatingItemTemplateDirective, context: unknown): context is RatingItemTemplateContext;
279
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RatingItemTemplateDirective, never>;
280
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RatingItemTemplateDirective, "ng-template[monaRatingItemTemplate]", never, {}, {}, never, never, true, never>;
281
+ }
282
+
283
+ /**
284
+ * @description Renders the committed selected visual for rating items. Overrides the default
285
+ * preset overlay whenever the visible state comes from the committed value.
286
+ */
287
+ declare class RatingSelectedItemTemplateDirective {
288
+ readonly templateRef: TemplateRef<RatingItemTemplateContext>;
289
+ static ngTemplateContextGuard(_directive: RatingSelectedItemTemplateDirective, context: unknown): context is RatingItemTemplateContext;
290
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<RatingSelectedItemTemplateDirective, never>;
291
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<RatingSelectedItemTemplateDirective, "ng-template[monaRatingSelectedItemTemplate]", never, {}, {}, never, never, true, never>;
292
+ }
293
+
294
+ export { RatingComponent, RatingHoveredItemTemplateDirective, RatingItemTemplateDirective, RatingSelectedItemTemplateDirective };
295
+ export type { RatingIconName, RatingItemTemplateContext, RatingLabelPosition, RatingPrecision, RatingSelectionMode, RatingVariantProps };
@@ -1,8 +1,10 @@
1
1
  import * as _angular_core from '@angular/core';
2
+ import { TemplateRef } from '@angular/core';
2
3
  import { FormValueControl } from '@angular/forms/signals';
3
4
  import * as class_variance_authority_types from 'class-variance-authority/types';
4
5
  import { VariantProps } from 'class-variance-authority';
5
6
  import { VariantInputs } from '@nanahoshi/mona-ui/internal';
7
+ import * as _nanahoshi_mona_ui_segmented from '@nanahoshi/mona-ui/segmented';
6
8
 
7
9
  type SegmentedValue = string | number;
8
10
 
@@ -12,6 +14,32 @@ interface SegmentedOption<T extends SegmentedValue = SegmentedValue> {
12
14
  readonly disabled?: boolean;
13
15
  }
14
16
 
17
+ /**
18
+ * @description Rendering context passed to the segmented item template.
19
+ */
20
+ interface SegmentedItemTemplateContext<T extends SegmentedValue = SegmentedValue> {
21
+ /**
22
+ * The option rendered by this item.
23
+ */
24
+ readonly $implicit: SegmentedOption<T>;
25
+ /**
26
+ * The option rendered by this item.
27
+ */
28
+ readonly option: SegmentedOption<T>;
29
+ /**
30
+ * Zero-based option index.
31
+ */
32
+ readonly index: number;
33
+ /**
34
+ * Whether this option is currently selected.
35
+ */
36
+ readonly selected: boolean;
37
+ /**
38
+ * Whether this option is disabled, either individually or via the group.
39
+ */
40
+ readonly disabled: boolean;
41
+ }
42
+
15
43
  declare const segmentedContainerThemeVariants: (props?: ({
16
44
  rounded?: "none" | "small" | "medium" | "large" | "full" | null | undefined;
17
45
  } & class_variance_authority_types.ClassProp) | undefined) => string;
@@ -31,6 +59,7 @@ declare class SegmentedComponent<T extends SegmentedValue = SegmentedValue> impl
31
59
  protected readonly groupName: string;
32
60
  protected readonly inputClasses: _angular_core.Signal<string>;
33
61
  protected readonly invalidState: _angular_core.Signal<boolean>;
62
+ protected readonly itemTemplate: _angular_core.Signal<TemplateRef<any> | undefined>;
34
63
  protected readonly optionClasses: _angular_core.Signal<string>;
35
64
  /**
36
65
  * @description Accessible name for the radio group. Provide either `aria-label` or `aria-labelledby`.
@@ -88,11 +117,23 @@ declare class SegmentedComponent<T extends SegmentedValue = SegmentedValue> impl
88
117
  * @default null
89
118
  */
90
119
  readonly value: _angular_core.ModelSignal<T | null>;
120
+ protected createOptionContext(option: SegmentedOption<T>, index: number): SegmentedItemTemplateContext<T>;
91
121
  protected onOptionBlur(): void;
92
122
  protected onOptionChange(option: SegmentedOption<T>): void;
93
123
  static ɵfac: _angular_core.ɵɵFactoryDeclaration<SegmentedComponent<any>, never>;
94
- static ɵcmp: _angular_core.ɵɵComponentDeclaration<SegmentedComponent<any>, "mona-segmented", never, { "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "ariaLabelledBy": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": true; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "touch": "touch"; "value": "valueChange"; }, never, never, true, never>;
124
+ static ɵcmp: _angular_core.ɵɵComponentDeclaration<SegmentedComponent<any>, "mona-segmented", never, { "ariaLabel": { "alias": "aria-label"; "required": false; "isSignal": true; }; "ariaLabelledBy": { "alias": "aria-labelledby"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "invalid": { "alias": "invalid"; "required": false; "isSignal": true; }; "options": { "alias": "options"; "required": true; "isSignal": true; }; "rounded": { "alias": "rounded"; "required": false; "isSignal": true; }; "size": { "alias": "size"; "required": false; "isSignal": true; }; "touched": { "alias": "touched"; "required": false; "isSignal": true; }; "userClass": { "alias": "class"; "required": false; "isSignal": true; }; "value": { "alias": "value"; "required": false; "isSignal": true; }; }, { "touch": "touch"; "value": "valueChange"; }, ["itemTemplate"], never, true, never>;
125
+ }
126
+
127
+ /**
128
+ * @description Renders the visual content of every segmented option. Overrides the default label
129
+ * text of each option.
130
+ */
131
+ declare class SegmentedItemTemplateDirective {
132
+ readonly templateRef: TemplateRef<SegmentedItemTemplateContext<_nanahoshi_mona_ui_segmented.SegmentedValue>>;
133
+ static ngTemplateContextGuard(_directive: SegmentedItemTemplateDirective, context: unknown): context is SegmentedItemTemplateContext;
134
+ static ɵfac: _angular_core.ɵɵFactoryDeclaration<SegmentedItemTemplateDirective, never>;
135
+ static ɵdir: _angular_core.ɵɵDirectiveDeclaration<SegmentedItemTemplateDirective, "ng-template[monaSegmentedItemTemplate]", never, {}, {}, never, never, true, never>;
95
136
  }
96
137
 
97
- export { SegmentedComponent };
98
- export type { SegmentedOption, SegmentedValue, SegmentedVariantProps };
138
+ export { SegmentedComponent, SegmentedItemTemplateDirective };
139
+ export type { SegmentedItemTemplateContext, SegmentedOption, SegmentedValue, SegmentedVariantProps };