@joanroucoux/cairn-ui 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/fesm2022/joanroucoux-cairn-ui-action-bar.mjs +54 -0
  3. package/fesm2022/joanroucoux-cairn-ui-action-bar.mjs.map +1 -0
  4. package/fesm2022/joanroucoux-cairn-ui-alert.mjs +129 -0
  5. package/fesm2022/joanroucoux-cairn-ui-alert.mjs.map +1 -0
  6. package/fesm2022/joanroucoux-cairn-ui-amount.mjs +114 -0
  7. package/fesm2022/joanroucoux-cairn-ui-amount.mjs.map +1 -0
  8. package/fesm2022/joanroucoux-cairn-ui-async.mjs +282 -0
  9. package/fesm2022/joanroucoux-cairn-ui-async.mjs.map +1 -0
  10. package/fesm2022/joanroucoux-cairn-ui-avatar.mjs +71 -0
  11. package/fesm2022/joanroucoux-cairn-ui-avatar.mjs.map +1 -0
  12. package/fesm2022/joanroucoux-cairn-ui-back-link.mjs +79 -0
  13. package/fesm2022/joanroucoux-cairn-ui-back-link.mjs.map +1 -0
  14. package/fesm2022/joanroucoux-cairn-ui-badge.mjs +48 -0
  15. package/fesm2022/joanroucoux-cairn-ui-badge.mjs.map +1 -0
  16. package/fesm2022/joanroucoux-cairn-ui-button.mjs +128 -0
  17. package/fesm2022/joanroucoux-cairn-ui-button.mjs.map +1 -0
  18. package/fesm2022/joanroucoux-cairn-ui-card.mjs +146 -0
  19. package/fesm2022/joanroucoux-cairn-ui-card.mjs.map +1 -0
  20. package/fesm2022/joanroucoux-cairn-ui-choice-chips.mjs +172 -0
  21. package/fesm2022/joanroucoux-cairn-ui-choice-chips.mjs.map +1 -0
  22. package/fesm2022/joanroucoux-cairn-ui-control.mjs +18 -0
  23. package/fesm2022/joanroucoux-cairn-ui-control.mjs.map +1 -0
  24. package/fesm2022/joanroucoux-cairn-ui-delta.mjs +103 -0
  25. package/fesm2022/joanroucoux-cairn-ui-delta.mjs.map +1 -0
  26. package/fesm2022/joanroucoux-cairn-ui-dialog.mjs +626 -0
  27. package/fesm2022/joanroucoux-cairn-ui-dialog.mjs.map +1 -0
  28. package/fesm2022/joanroucoux-cairn-ui-donut.mjs +347 -0
  29. package/fesm2022/joanroucoux-cairn-ui-donut.mjs.map +1 -0
  30. package/fesm2022/joanroucoux-cairn-ui-empty.mjs +47 -0
  31. package/fesm2022/joanroucoux-cairn-ui-empty.mjs.map +1 -0
  32. package/fesm2022/joanroucoux-cairn-ui-external-link.mjs +65 -0
  33. package/fesm2022/joanroucoux-cairn-ui-external-link.mjs.map +1 -0
  34. package/fesm2022/joanroucoux-cairn-ui-fact.mjs +99 -0
  35. package/fesm2022/joanroucoux-cairn-ui-fact.mjs.map +1 -0
  36. package/fesm2022/joanroucoux-cairn-ui-field.mjs +232 -0
  37. package/fesm2022/joanroucoux-cairn-ui-field.mjs.map +1 -0
  38. package/fesm2022/joanroucoux-cairn-ui-filter-chips.mjs +89 -0
  39. package/fesm2022/joanroucoux-cairn-ui-filter-chips.mjs.map +1 -0
  40. package/fesm2022/joanroucoux-cairn-ui-input.mjs +90 -0
  41. package/fesm2022/joanroucoux-cairn-ui-input.mjs.map +1 -0
  42. package/fesm2022/joanroucoux-cairn-ui-line-chart.mjs +844 -0
  43. package/fesm2022/joanroucoux-cairn-ui-line-chart.mjs.map +1 -0
  44. package/fesm2022/joanroucoux-cairn-ui-menu.mjs +241 -0
  45. package/fesm2022/joanroucoux-cairn-ui-menu.mjs.map +1 -0
  46. package/fesm2022/joanroucoux-cairn-ui-meter.mjs +86 -0
  47. package/fesm2022/joanroucoux-cairn-ui-meter.mjs.map +1 -0
  48. package/fesm2022/joanroucoux-cairn-ui-motion.mjs +415 -0
  49. package/fesm2022/joanroucoux-cairn-ui-motion.mjs.map +1 -0
  50. package/fesm2022/joanroucoux-cairn-ui-nav-item.mjs +45 -0
  51. package/fesm2022/joanroucoux-cairn-ui-nav-item.mjs.map +1 -0
  52. package/fesm2022/joanroucoux-cairn-ui-row.mjs +164 -0
  53. package/fesm2022/joanroucoux-cairn-ui-row.mjs.map +1 -0
  54. package/fesm2022/joanroucoux-cairn-ui-segmented.mjs +165 -0
  55. package/fesm2022/joanroucoux-cairn-ui-segmented.mjs.map +1 -0
  56. package/fesm2022/joanroucoux-cairn-ui-select.mjs +69 -0
  57. package/fesm2022/joanroucoux-cairn-ui-select.mjs.map +1 -0
  58. package/fesm2022/joanroucoux-cairn-ui-skeleton.mjs +91 -0
  59. package/fesm2022/joanroucoux-cairn-ui-skeleton.mjs.map +1 -0
  60. package/fesm2022/joanroucoux-cairn-ui-stale-link.mjs +43 -0
  61. package/fesm2022/joanroucoux-cairn-ui-stale-link.mjs.map +1 -0
  62. package/fesm2022/joanroucoux-cairn-ui-stat.mjs +96 -0
  63. package/fesm2022/joanroucoux-cairn-ui-stat.mjs.map +1 -0
  64. package/fesm2022/joanroucoux-cairn-ui-switch.mjs +37 -0
  65. package/fesm2022/joanroucoux-cairn-ui-switch.mjs.map +1 -0
  66. package/fesm2022/joanroucoux-cairn-ui-tab-bar.mjs +75 -0
  67. package/fesm2022/joanroucoux-cairn-ui-tab-bar.mjs.map +1 -0
  68. package/fesm2022/joanroucoux-cairn-ui-table.mjs +321 -0
  69. package/fesm2022/joanroucoux-cairn-ui-table.mjs.map +1 -0
  70. package/fesm2022/joanroucoux-cairn-ui-toast.mjs +153 -0
  71. package/fesm2022/joanroucoux-cairn-ui-toast.mjs.map +1 -0
  72. package/fesm2022/joanroucoux-cairn-ui.mjs +0 -5386
  73. package/fesm2022/joanroucoux-cairn-ui.mjs.map +1 -1
  74. package/package.json +141 -1
  75. package/styles/motion.css +9 -0
  76. package/types/joanroucoux-cairn-ui-action-bar.d.ts +22 -0
  77. package/types/joanroucoux-cairn-ui-alert.d.ts +23 -0
  78. package/types/joanroucoux-cairn-ui-amount.d.ts +45 -0
  79. package/types/joanroucoux-cairn-ui-async.d.ts +58 -0
  80. package/types/joanroucoux-cairn-ui-avatar.d.ts +35 -0
  81. package/types/joanroucoux-cairn-ui-back-link.d.ts +22 -0
  82. package/types/joanroucoux-cairn-ui-badge.d.ts +23 -0
  83. package/types/joanroucoux-cairn-ui-button.d.ts +29 -0
  84. package/types/joanroucoux-cairn-ui-card.d.ts +36 -0
  85. package/types/joanroucoux-cairn-ui-choice-chips.d.ts +46 -0
  86. package/types/joanroucoux-cairn-ui-control.d.ts +27 -0
  87. package/types/joanroucoux-cairn-ui-delta.d.ts +38 -0
  88. package/types/joanroucoux-cairn-ui-dialog.d.ts +72 -0
  89. package/types/joanroucoux-cairn-ui-donut.d.ts +59 -0
  90. package/types/joanroucoux-cairn-ui-empty.d.ts +19 -0
  91. package/types/joanroucoux-cairn-ui-external-link.d.ts +15 -0
  92. package/types/joanroucoux-cairn-ui-fact.d.ts +44 -0
  93. package/types/joanroucoux-cairn-ui-field.d.ts +84 -0
  94. package/types/joanroucoux-cairn-ui-filter-chips.d.ts +28 -0
  95. package/types/joanroucoux-cairn-ui-input.d.ts +51 -0
  96. package/types/joanroucoux-cairn-ui-line-chart.d.ts +114 -0
  97. package/types/joanroucoux-cairn-ui-menu.d.ts +66 -0
  98. package/types/joanroucoux-cairn-ui-meter.d.ts +30 -0
  99. package/types/joanroucoux-cairn-ui-motion.d.ts +124 -0
  100. package/types/joanroucoux-cairn-ui-nav-item.d.ts +21 -0
  101. package/types/joanroucoux-cairn-ui-row.d.ts +78 -0
  102. package/types/joanroucoux-cairn-ui-segmented.d.ts +36 -0
  103. package/types/joanroucoux-cairn-ui-select.d.ts +23 -0
  104. package/types/joanroucoux-cairn-ui-skeleton.d.ts +26 -0
  105. package/types/joanroucoux-cairn-ui-stale-link.d.ts +24 -0
  106. package/types/joanroucoux-cairn-ui-stat.d.ts +32 -0
  107. package/types/joanroucoux-cairn-ui-switch.d.ts +17 -0
  108. package/types/joanroucoux-cairn-ui-tab-bar.d.ts +31 -0
  109. package/types/joanroucoux-cairn-ui-table.d.ts +139 -0
  110. package/types/joanroucoux-cairn-ui-toast.d.ts +51 -0
  111. package/types/joanroucoux-cairn-ui.d.ts +1 -1407
@@ -0,0 +1,99 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, booleanAttribute, computed, Directive, Component } from '@angular/core';
3
+
4
+ /** Available fact row sizes. `FactSize` is derived from this tuple. */
5
+ const FACT_SIZES = ['md', 'sm', 'auto', 'recap'];
6
+ /** Colour of the line under a fact value. `stale` flags an out-of-date figure. */
7
+ const FACT_SUB_TONES = ['subtle', 'stale'];
8
+ const SIZE_CLASSES = {
9
+ md: { row: 'gap-4 py-3 [&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]', value: 'text-body' },
10
+ sm: { row: 'gap-4 py-2.5', value: 'text-label' },
11
+ auto: {
12
+ row: 'gap-4 py-3 lg:py-2.5 max-lg:[&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]',
13
+ value: 'text-body lg:text-label',
14
+ },
15
+ recap: { row: 'gap-3 py-[9px] [&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]', value: 'text-label' },
16
+ };
17
+ const SUB_TONE_CLASSES = {
18
+ subtle: 'text-(--subtle-foreground)',
19
+ stale: 'text-(--stale)',
20
+ };
21
+ /**
22
+ * The list that holds `div[ui-fact]` rows, as a native description list.
23
+ *
24
+ * @example
25
+ * <dl uiFacts>
26
+ * <div ui-fact label="Quantity">500</div>
27
+ * <div ui-fact label="Price" sub="24/09 · Yahoo">28,64 €</div>
28
+ * </dl>
29
+ */
30
+ class UiFacts {
31
+ ruled = input(false, { ...(ngDevMode ? { debugName: "ruled" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
32
+ classes = computed(() => this.ruled() ? 'm-0 flex flex-col shadow-[inset_0_1px_0_var(--hairline)]' : 'm-0 flex flex-col', /* @ts-ignore */
33
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
34
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFacts, deps: [], target: i0.ɵɵFactoryTarget.Directive });
35
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiFacts, isStandalone: true, selector: "dl[uiFacts]", inputs: { ruled: { classPropertyName: "ruled", publicName: "ruled", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0 });
36
+ }
37
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFacts, decorators: [{
38
+ type: Directive,
39
+ args: [{
40
+ selector: 'dl[uiFacts]',
41
+ host: { '[class]': 'classes()' },
42
+ }]
43
+ }], propDecorators: { ruled: [{ type: i0.Input, args: [{ isSignal: true, alias: "ruled", required: false }] }] } });
44
+ /**
45
+ * One fact of a `dl[uiFacts]`: a label on the left, its value on the right, an optional sub-line
46
+ * under the value, and a hairline above every row but the first at the `md` size (the `sm` desktop list is separated by space only). The projected content is the value.
47
+ *
48
+ * @example
49
+ * <div ui-fact label="Price" sub="24/09 · Yahoo" subTone="stale">28,64 €</div>
50
+ */
51
+ class UiFact {
52
+ label = input.required(/* @ts-ignore */
53
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
54
+ sub = input(/* @ts-ignore */
55
+ ...(ngDevMode ? [undefined, { debugName: "sub" }] : /* istanbul ignore next */ []));
56
+ subTone = input('subtle', /* @ts-ignore */
57
+ ...(ngDevMode ? [{ debugName: "subTone" }] : /* istanbul ignore next */ []));
58
+ size = input('md', /* @ts-ignore */
59
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
60
+ classes = computed(() => `flex items-baseline justify-between ${SIZE_CLASSES[this.size()].row}`, /* @ts-ignore */
61
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
62
+ valueClasses = computed(() => `${SIZE_CLASSES[this.size()].value} tabular-nums`, /* @ts-ignore */
63
+ ...(ngDevMode ? [{ debugName: "valueClasses" }] : /* istanbul ignore next */ []));
64
+ subClasses = computed(() => `text-caption ${SUB_TONE_CLASSES[this.subTone()]}`, /* @ts-ignore */
65
+ ...(ngDevMode ? [{ debugName: "subClasses" }] : /* istanbul ignore next */ []));
66
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFact, deps: [], target: i0.ɵɵFactoryTarget.Component });
67
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiFact, isStandalone: true, selector: "div[ui-fact]", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, sub: { classPropertyName: "sub", publicName: "sub", isSignal: true, isRequired: false, transformFunction: null }, subTone: { classPropertyName: "subTone", publicName: "subTone", isSignal: true, isRequired: false, transformFunction: null }, size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, ngImport: i0, template: `
68
+ <dt class="text-label text-(--muted-foreground)">{{ label() }}</dt>
69
+ <dd class="m-0 flex flex-col items-end text-right">
70
+ <span [class]="valueClasses()"><ng-content /></span>
71
+ @if (sub()) {
72
+ <span [class]="subClasses()">{{ sub() }}</span>
73
+ }
74
+ </dd>
75
+ `, isInline: true });
76
+ }
77
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFact, decorators: [{
78
+ type: Component,
79
+ args: [{
80
+ selector: 'div[ui-fact]',
81
+ template: `
82
+ <dt class="text-label text-(--muted-foreground)">{{ label() }}</dt>
83
+ <dd class="m-0 flex flex-col items-end text-right">
84
+ <span [class]="valueClasses()"><ng-content /></span>
85
+ @if (sub()) {
86
+ <span [class]="subClasses()">{{ sub() }}</span>
87
+ }
88
+ </dd>
89
+ `,
90
+ host: { '[class]': 'classes()' },
91
+ }]
92
+ }], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], sub: [{ type: i0.Input, args: [{ isSignal: true, alias: "sub", required: false }] }], subTone: [{ type: i0.Input, args: [{ isSignal: true, alias: "subTone", required: false }] }], size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }] } });
93
+
94
+ /**
95
+ * Generated bundle index. Do not edit.
96
+ */
97
+
98
+ export { FACT_SIZES, FACT_SUB_TONES, UiFact, UiFacts };
99
+ //# sourceMappingURL=joanroucoux-cairn-ui-fact.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joanroucoux-cairn-ui-fact.mjs","sources":["../../../projects/ui/fact/fact.ts","../../../projects/ui/fact/joanroucoux-cairn-ui-fact.ts"],"sourcesContent":["import { Component, Directive, booleanAttribute, computed, input } from '@angular/core';\n\n/** Available fact row sizes. `FactSize` is derived from this tuple. */\nexport const FACT_SIZES = ['md', 'sm', 'auto', 'recap'] as const;\nexport type FactSize = (typeof FACT_SIZES)[number];\n\n/** Colour of the line under a fact value. `stale` flags an out-of-date figure. */\nexport const FACT_SUB_TONES = ['subtle', 'stale'] as const;\nexport type FactSubTone = (typeof FACT_SUB_TONES)[number];\n\nconst SIZE_CLASSES: Record<FactSize, { row: string; value: string }> = {\n md: { row: 'gap-4 py-3 [&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]', value: 'text-body' },\n sm: { row: 'gap-4 py-2.5', value: 'text-label' },\n auto: {\n row: 'gap-4 py-3 lg:py-2.5 max-lg:[&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]',\n value: 'text-body lg:text-label',\n },\n recap: { row: 'gap-3 py-[9px] [&:not(:first-child)]:shadow-[inset_0_1px_0_var(--hairline)]', value: 'text-label' },\n};\n\nconst SUB_TONE_CLASSES: Record<FactSubTone, string> = {\n subtle: 'text-(--subtle-foreground)',\n stale: 'text-(--stale)',\n};\n\n/**\n * The list that holds `div[ui-fact]` rows, as a native description list.\n *\n * @example\n * <dl uiFacts>\n * <div ui-fact label=\"Quantity\">500</div>\n * <div ui-fact label=\"Price\" sub=\"24/09 · Yahoo\">28,64 €</div>\n * </dl>\n */\n@Directive({\n selector: 'dl[uiFacts]',\n host: { '[class]': 'classes()' },\n})\nexport class UiFacts {\n readonly ruled = input(false, { transform: booleanAttribute });\n\n protected readonly classes = computed(() =>\n this.ruled() ? 'm-0 flex flex-col shadow-[inset_0_1px_0_var(--hairline)]' : 'm-0 flex flex-col',\n );\n}\n\n/**\n * One fact of a `dl[uiFacts]`: a label on the left, its value on the right, an optional sub-line\n * under the value, and a hairline above every row but the first at the `md` size (the `sm` desktop list is separated by space only). The projected content is the value.\n *\n * @example\n * <div ui-fact label=\"Price\" sub=\"24/09 · Yahoo\" subTone=\"stale\">28,64 €</div>\n */\n@Component({\n selector: 'div[ui-fact]',\n template: `\n <dt class=\"text-label text-(--muted-foreground)\">{{ label() }}</dt>\n <dd class=\"m-0 flex flex-col items-end text-right\">\n <span [class]=\"valueClasses()\"><ng-content /></span>\n @if (sub()) {\n <span [class]=\"subClasses()\">{{ sub() }}</span>\n }\n </dd>\n `,\n host: { '[class]': 'classes()' },\n})\nexport class UiFact {\n readonly label = input.required<string>();\n readonly sub = input<string>();\n readonly subTone = input<FactSubTone>('subtle');\n readonly size = input<FactSize>('md');\n\n protected readonly classes = computed(() => `flex items-baseline justify-between ${SIZE_CLASSES[this.size()].row}`);\n protected readonly valueClasses = computed(() => `${SIZE_CLASSES[this.size()].value} tabular-nums`);\n protected readonly subClasses = computed(() => `text-caption ${SUB_TONE_CLASSES[this.subTone()]}`);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,232 @@
1
+ import * as i0 from '@angular/core';
2
+ import { Directive, input, booleanAttribute, inject, ElementRef, contentChild, computed, afterRenderEffect, Component } from '@angular/core';
3
+ import { UI_CONTROL } from '@joanroucoux/cairn-ui/control';
4
+
5
+ const nextId = (() => {
6
+ let count = 0;
7
+ return () => `ui-field-${++count}`;
8
+ })();
9
+ /**
10
+ * Marks an element, typically an 18px svg icon, as the leading content of a `ui-field`: it sits
11
+ * inside the control's left edge and the control's text is inset to clear it. Decorative, so hidden
12
+ * from assistive technologies.
13
+ *
14
+ * @example
15
+ * <ui-field label="Search" labelHidden>
16
+ * <svg uiFieldLeading width="18" height="18" viewBox="0 0 24 24">...</svg>
17
+ * <input uiInput type="search" />
18
+ * </ui-field>
19
+ */
20
+ class UiFieldLeading {
21
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldLeading, deps: [], target: i0.ɵɵFactoryTarget.Directive });
22
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFieldLeading, isStandalone: true, selector: "[uiFieldLeading]", host: { attributes: { "aria-hidden": "true" }, classAttribute: "pointer-events-none absolute left-3 flex text-(--muted-foreground)" }, ngImport: i0 });
23
+ }
24
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldLeading, decorators: [{
25
+ type: Directive,
26
+ args: [{
27
+ selector: '[uiFieldLeading]',
28
+ host: {
29
+ class: 'pointer-events-none absolute left-3 flex text-(--muted-foreground)',
30
+ 'aria-hidden': 'true',
31
+ },
32
+ }]
33
+ }] });
34
+ /**
35
+ * Marks an element, typically an icon button, as the trailing action of a `ui-field`: it sits on the
36
+ * control's right edge, square to the control's height, and the control's text is padded to clear it.
37
+ *
38
+ * @example
39
+ * <ui-field label="Password">
40
+ * <input uiInput type="password" size="xl" />
41
+ * <button uiFieldTrailing type="button" aria-label="Show the password" [attr.aria-pressed]="shown()">...</button>
42
+ * </ui-field>
43
+ *
44
+ * A toggle such as this one should carry `aria-pressed`. The slot shares the right edge with `unit`:
45
+ * set one or the other, not both.
46
+ */
47
+ class UiFieldTrailing {
48
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, deps: [], target: i0.ɵɵFactoryTarget.Directive });
49
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.7", type: UiFieldTrailing, isStandalone: true, selector: "[uiFieldTrailing]", host: { classAttribute: "absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) transition-[color] duration-(--duration-fast) ease-out hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)" }, ngImport: i0 });
50
+ }
51
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFieldTrailing, decorators: [{
52
+ type: Directive,
53
+ args: [{
54
+ selector: '[uiFieldTrailing]',
55
+ host: {
56
+ class: 'absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) transition-[color] duration-(--duration-fast) ease-out hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)',
57
+ },
58
+ }]
59
+ }] });
60
+ /**
61
+ * Label, control, hint and error as one unit. The control stays a plain native element:
62
+ * the field finds it in its own projected content and wires the ARIA attributes onto it.
63
+ *
64
+ * The message comes from the caller's `error`, or, where none is set, from the projected control's
65
+ * own validation state once it is touched.
66
+ *
67
+ * @example
68
+ * <ui-field label="Quantity" hint="Leave empty if unknown">
69
+ * <input uiInput type="number" [formField]="form.quantity" />
70
+ * </ui-field>
71
+ *
72
+ * @example
73
+ * <ui-field label="ISIN or name" [error]="blank() ? 'Enter an ISIN' : undefined">
74
+ * <input uiInput type="search" />
75
+ * </ui-field>
76
+ *
77
+ * @example
78
+ * <ui-field label="Search holdings" labelHidden>
79
+ * <input uiInput type="search" placeholder="Instrument or account" />
80
+ * </ui-field>
81
+ */
82
+ class UiField {
83
+ label = input.required(/* @ts-ignore */
84
+ ...(ngDevMode ? [{ debugName: "label" }] : /* istanbul ignore next */ []));
85
+ labelHidden = input(false, { ...(ngDevMode ? { debugName: "labelHidden" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
86
+ hint = input(/* @ts-ignore */
87
+ ...(ngDevMode ? [undefined, { debugName: "hint" }] : /* istanbul ignore next */ []));
88
+ error = input(/* @ts-ignore */
89
+ ...(ngDevMode ? [undefined, { debugName: "error" }] : /* istanbul ignore next */ []));
90
+ unit = input(/* @ts-ignore */
91
+ ...(ngDevMode ? [undefined, { debugName: "unit" }] : /* istanbul ignore next */ []));
92
+ optional = input(/* @ts-ignore */
93
+ ...(ngDevMode ? [undefined, { debugName: "optional" }] : /* istanbul ignore next */ []));
94
+ invalid = input(false, { ...(ngDevMode ? { debugName: "invalid" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
95
+ #id = nextId();
96
+ hintId = `${this.#id}-hint`;
97
+ errorId = `${this.#id}-error`;
98
+ #host = inject(ElementRef);
99
+ control = contentChild(UI_CONTROL, /* @ts-ignore */
100
+ ...(ngDevMode ? [{ debugName: "control" }] : /* istanbul ignore next */ []));
101
+ leading = contentChild(UiFieldLeading, /* @ts-ignore */
102
+ ...(ngDevMode ? [{ debugName: "leading" }] : /* istanbul ignore next */ []));
103
+ trailing = contentChild(UiFieldTrailing, /* @ts-ignore */
104
+ ...(ngDevMode ? [{ debugName: "trailing" }] : /* istanbul ignore next */ []));
105
+ rowClasses = computed(() => [
106
+ 'relative flex items-center',
107
+ this.leading() ? '[&>input]:pl-[38px]' : '',
108
+ this.trailing() ? '[&>input]:pr-12 lg:[&>input]:pr-11' : '',
109
+ ]
110
+ .filter(Boolean)
111
+ .join(' '), /* @ts-ignore */
112
+ ...(ngDevMode ? [{ debugName: "rowClasses" }] : /* istanbul ignore next */ []));
113
+ labelClasses = computed(() => this.labelHidden() ? 'sr-only' : 'text-label leading-[17px] font-medium text-(--muted-foreground)', /* @ts-ignore */
114
+ ...(ngDevMode ? [{ debugName: "labelClasses" }] : /* istanbul ignore next */ []));
115
+ /**
116
+ * The caller's own `error` wins: it is the only way to report something the control does not
117
+ * know about, such as a search box refusing an empty query.
118
+ */
119
+ message = computed(() => {
120
+ const explicit = this.error();
121
+ if (explicit) {
122
+ return explicit;
123
+ }
124
+ const control = this.control();
125
+ if (!control?.touched()) {
126
+ return undefined;
127
+ }
128
+ return control.errors().find((error) => error.message)?.message;
129
+ }, /* @ts-ignore */
130
+ ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
131
+ constructor() {
132
+ let markedByField = false;
133
+ afterRenderEffect(() => {
134
+ // querySelector, not viewChild.required, so the "no control found" guard below stays reachable and testable.
135
+ const control = this.#host.nativeElement.querySelector('input, select, textarea');
136
+ if (!control) {
137
+ return;
138
+ }
139
+ control.id ||= this.#id;
140
+ this.#host.nativeElement.querySelector('label').htmlFor = control.id;
141
+ const describedBy = [this.hint() ? this.hintId : null, this.message() ? this.errorId : null].filter(Boolean);
142
+ if (describedBy.length > 0) {
143
+ control.setAttribute('aria-describedby', describedBy.join(' '));
144
+ }
145
+ else {
146
+ control.removeAttribute('aria-describedby');
147
+ }
148
+ if (this.message() || this.invalid()) {
149
+ control.setAttribute('aria-invalid', 'true');
150
+ markedByField = true;
151
+ }
152
+ else if (markedByField) {
153
+ control.removeAttribute('aria-invalid');
154
+ markedByField = false;
155
+ }
156
+ });
157
+ }
158
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, deps: [], target: i0.ɵɵFactoryTarget.Component });
159
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiField, isStandalone: true, selector: "ui-field", inputs: { label: { classPropertyName: "label", publicName: "label", isSignal: true, isRequired: true, transformFunction: null }, labelHidden: { classPropertyName: "labelHidden", publicName: "labelHidden", isSignal: true, isRequired: false, transformFunction: null }, hint: { classPropertyName: "hint", publicName: "hint", isSignal: true, isRequired: false, transformFunction: null }, error: { classPropertyName: "error", publicName: "error", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null }, optional: { classPropertyName: "optional", publicName: "optional", isSignal: true, isRequired: false, transformFunction: null }, invalid: { classPropertyName: "invalid", publicName: "invalid", isSignal: true, isRequired: false, transformFunction: null } }, queries: [{ propertyName: "control", first: true, predicate: UI_CONTROL, descendants: true, isSignal: true }, { propertyName: "leading", first: true, predicate: UiFieldLeading, descendants: true, isSignal: true }, { propertyName: "trailing", first: true, predicate: UiFieldTrailing, descendants: true, isSignal: true }], ngImport: i0, template: `
160
+ <div class="group flex flex-col gap-1.5">
161
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
162
+ <label [class]="labelClasses()"
163
+ >{{ label() }}
164
+ @if (optional()) {
165
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
166
+ }
167
+ </label>
168
+
169
+ <div [class]="rowClasses()">
170
+ <ng-content select="[uiFieldLeading]" />
171
+ <ng-content />
172
+ <ng-content select="[uiFieldTrailing]" />
173
+ @if (unit()) {
174
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
175
+ }
176
+ </div>
177
+
178
+ @if (hint()) {
179
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
180
+ }
181
+
182
+ @if (message()) {
183
+ <p animate.enter="ui-enter-fade" class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">
184
+ {{ message() }}
185
+ </p>
186
+ }
187
+ </div>
188
+ `, isInline: true });
189
+ }
190
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiField, decorators: [{
191
+ type: Component,
192
+ args: [{
193
+ selector: 'ui-field',
194
+ template: `
195
+ <div class="group flex flex-col gap-1.5">
196
+ <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->
197
+ <label [class]="labelClasses()"
198
+ >{{ label() }}
199
+ @if (optional()) {
200
+ {{ ' ' }}<span class="font-normal text-(--subtle-foreground)">{{ optional() }}</span>
201
+ }
202
+ </label>
203
+
204
+ <div [class]="rowClasses()">
205
+ <ng-content select="[uiFieldLeading]" />
206
+ <ng-content />
207
+ <ng-content select="[uiFieldTrailing]" />
208
+ @if (unit()) {
209
+ <span class="text-label pointer-events-none absolute right-3 text-(--subtle-foreground)">{{ unit() }}</span>
210
+ }
211
+ </div>
212
+
213
+ @if (hint()) {
214
+ <span class="text-label text-(--muted-foreground)" [id]="hintId">{{ hint() }}</span>
215
+ }
216
+
217
+ @if (message()) {
218
+ <p animate.enter="ui-enter-fade" class="text-label font-medium text-(--negative)" role="alert" [id]="errorId">
219
+ {{ message() }}
220
+ </p>
221
+ }
222
+ </div>
223
+ `,
224
+ }]
225
+ }], ctorParameters: () => [], propDecorators: { label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: true }] }], labelHidden: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelHidden", required: false }] }], hint: [{ type: i0.Input, args: [{ isSignal: true, alias: "hint", required: false }] }], error: [{ type: i0.Input, args: [{ isSignal: true, alias: "error", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }], optional: [{ type: i0.Input, args: [{ isSignal: true, alias: "optional", required: false }] }], invalid: [{ type: i0.Input, args: [{ isSignal: true, alias: "invalid", required: false }] }], control: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UI_CONTROL), { isSignal: true }] }], leading: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldLeading), { isSignal: true }] }], trailing: [{ type: i0.ContentChild, args: [i0.forwardRef(() => UiFieldTrailing), { isSignal: true }] }] } });
226
+
227
+ /**
228
+ * Generated bundle index. Do not edit.
229
+ */
230
+
231
+ export { UiField, UiFieldLeading, UiFieldTrailing };
232
+ //# sourceMappingURL=joanroucoux-cairn-ui-field.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joanroucoux-cairn-ui-field.mjs","sources":["../../../projects/ui/field/field.ts","../../../projects/ui/field/joanroucoux-cairn-ui-field.ts"],"sourcesContent":["import {\n Component,\n Directive,\n ElementRef,\n afterRenderEffect,\n booleanAttribute,\n computed,\n contentChild,\n inject,\n input,\n} from '@angular/core';\n\nimport { UI_CONTROL } from '@joanroucoux/cairn-ui/control';\n\nconst nextId = (() => {\n let count = 0;\n\n return (): string => `ui-field-${++count}`;\n})();\n\n/**\n * Marks an element, typically an 18px svg icon, as the leading content of a `ui-field`: it sits\n * inside the control's left edge and the control's text is inset to clear it. Decorative, so hidden\n * from assistive technologies.\n *\n * @example\n * <ui-field label=\"Search\" labelHidden>\n * <svg uiFieldLeading width=\"18\" height=\"18\" viewBox=\"0 0 24 24\">...</svg>\n * <input uiInput type=\"search\" />\n * </ui-field>\n */\n@Directive({\n selector: '[uiFieldLeading]',\n host: {\n class: 'pointer-events-none absolute left-3 flex text-(--muted-foreground)',\n 'aria-hidden': 'true',\n },\n})\nexport class UiFieldLeading {}\n\n/**\n * Marks an element, typically an icon button, as the trailing action of a `ui-field`: it sits on the\n * control's right edge, square to the control's height, and the control's text is padded to clear it.\n *\n * @example\n * <ui-field label=\"Password\">\n * <input uiInput type=\"password\" size=\"xl\" />\n * <button uiFieldTrailing type=\"button\" aria-label=\"Show the password\" [attr.aria-pressed]=\"shown()\">...</button>\n * </ui-field>\n *\n * A toggle such as this one should carry `aria-pressed`. The slot shares the right edge with `unit`:\n * set one or the other, not both.\n */\n@Directive({\n selector: '[uiFieldTrailing]',\n host: {\n class:\n 'absolute right-0 grid h-full aspect-square place-items-center rounded-control cursor-pointer text-(--muted-foreground) transition-[color] duration-(--duration-fast) ease-out hover:text-(--foreground) focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-(--ring)',\n },\n})\nexport class UiFieldTrailing {}\n\n/**\n * Label, control, hint and error as one unit. The control stays a plain native element:\n * the field finds it in its own projected content and wires the ARIA attributes onto it.\n *\n * The message comes from the caller's `error`, or, where none is set, from the projected control's\n * own validation state once it is touched.\n *\n * @example\n * <ui-field label=\"Quantity\" hint=\"Leave empty if unknown\">\n * <input uiInput type=\"number\" [formField]=\"form.quantity\" />\n * </ui-field>\n *\n * @example\n * <ui-field label=\"ISIN or name\" [error]=\"blank() ? 'Enter an ISIN' : undefined\">\n * <input uiInput type=\"search\" />\n * </ui-field>\n *\n * @example\n * <ui-field label=\"Search holdings\" labelHidden>\n * <input uiInput type=\"search\" placeholder=\"Instrument or account\" />\n * </ui-field>\n */\n@Component({\n selector: 'ui-field',\n template: `\n <div class=\"group flex flex-col gap-1.5\">\n <!-- eslint-disable-next-line @angular-eslint/template/label-has-associated-control -- htmlFor is wired at runtime, once the projected control's id is known -->\n <label [class]=\"labelClasses()\"\n >{{ label() }}\n @if (optional()) {\n {{ ' ' }}<span class=\"font-normal text-(--subtle-foreground)\">{{ optional() }}</span>\n }\n </label>\n\n <div [class]=\"rowClasses()\">\n <ng-content select=\"[uiFieldLeading]\" />\n <ng-content />\n <ng-content select=\"[uiFieldTrailing]\" />\n @if (unit()) {\n <span class=\"text-label pointer-events-none absolute right-3 text-(--subtle-foreground)\">{{ unit() }}</span>\n }\n </div>\n\n @if (hint()) {\n <span class=\"text-label text-(--muted-foreground)\" [id]=\"hintId\">{{ hint() }}</span>\n }\n\n @if (message()) {\n <p animate.enter=\"ui-enter-fade\" class=\"text-label font-medium text-(--negative)\" role=\"alert\" [id]=\"errorId\">\n {{ message() }}\n </p>\n }\n </div>\n `,\n})\nexport class UiField {\n readonly label = input.required<string>();\n readonly labelHidden = input(false, { transform: booleanAttribute });\n readonly hint = input<string>();\n readonly error = input<string>();\n readonly unit = input<string>();\n readonly optional = input<string>();\n readonly invalid = input(false, { transform: booleanAttribute });\n\n readonly #id = nextId();\n protected readonly hintId = `${this.#id}-hint`;\n protected readonly errorId = `${this.#id}-error`;\n readonly #host = inject<ElementRef<HTMLElement>>(ElementRef);\n protected readonly control = contentChild(UI_CONTROL);\n protected readonly leading = contentChild(UiFieldLeading);\n\n protected readonly trailing = contentChild(UiFieldTrailing);\n\n protected readonly rowClasses = computed(() =>\n [\n 'relative flex items-center',\n this.leading() ? '[&>input]:pl-[38px]' : '',\n this.trailing() ? '[&>input]:pr-12 lg:[&>input]:pr-11' : '',\n ]\n .filter(Boolean)\n .join(' '),\n );\n\n protected readonly labelClasses = computed(() =>\n this.labelHidden() ? 'sr-only' : 'text-label leading-[17px] font-medium text-(--muted-foreground)',\n );\n\n /**\n * The caller's own `error` wins: it is the only way to report something the control does not\n * know about, such as a search box refusing an empty query.\n */\n protected readonly message = computed(() => {\n const explicit = this.error();\n\n if (explicit) {\n return explicit;\n }\n\n const control = this.control();\n\n if (!control?.touched()) {\n return undefined;\n }\n\n return control.errors().find((error) => error.message)?.message;\n });\n\n constructor() {\n let markedByField = false;\n\n afterRenderEffect(() => {\n // querySelector, not viewChild.required, so the \"no control found\" guard below stays reachable and testable.\n const control = this.#host.nativeElement.querySelector<HTMLElement>('input, select, textarea');\n\n if (!control) {\n return;\n }\n\n control.id ||= this.#id;\n this.#host.nativeElement.querySelector('label')!.htmlFor = control.id;\n\n const describedBy = [this.hint() ? this.hintId : null, this.message() ? this.errorId : null].filter(Boolean);\n\n if (describedBy.length > 0) {\n control.setAttribute('aria-describedby', describedBy.join(' '));\n } else {\n control.removeAttribute('aria-describedby');\n }\n\n if (this.message() || this.invalid()) {\n control.setAttribute('aria-invalid', 'true');\n markedByField = true;\n } else if (markedByField) {\n control.removeAttribute('aria-invalid');\n markedByField = false;\n }\n });\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, model, Component } from '@angular/core';
3
+
4
+ const CHIP_CLASSES = 'flex-none flex h-11 items-center rounded-pill border-0 bg-transparent p-0 font-[inherit] cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) pointer-fine:h-8 pointer-fine:focus-visible:outline-offset-2';
5
+ const PILL_CLASSES = 'flex h-[34px] items-center gap-1.5 rounded-pill px-3.5 text-label font-medium whitespace-nowrap pointer-fine:h-8 pointer-fine:px-3 pointer-fine:group-hover/chip:shadow-[inset_0_0_0_1px_var(--muted-foreground)]';
6
+ const SELECTED_PILL_CLASSES = 'bg-(--primary) text-(--primary-foreground)';
7
+ const UNSELECTED_PILL_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';
8
+ /**
9
+ * A row of single-choice filter pills, each with an optional count, that narrows a list in place:
10
+ * an asset class, a status. On touch it scrolls on one line, bleeding to the screen edges; with a
11
+ * fine pointer it wraps.
12
+ *
13
+ * @example
14
+ * <ui-filter-chips ariaLabel="Filter by class" [options]="classes()" [(value)]="selectedClass" />
15
+ */
16
+ class UiFilterChips {
17
+ options = input.required(/* @ts-ignore */
18
+ ...(ngDevMode ? [{ debugName: "options" }] : /* istanbul ignore next */ []));
19
+ value = model.required(/* @ts-ignore */
20
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
21
+ ariaLabel = input.required(/* @ts-ignore */
22
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
23
+ chipClasses = CHIP_CLASSES;
24
+ pillClasses(optionValue) {
25
+ return `${PILL_CLASSES} ${optionValue === this.value() ? SELECTED_PILL_CLASSES : UNSELECTED_PILL_CLASSES}`;
26
+ }
27
+ countClasses(optionValue) {
28
+ return optionValue === this.value() ? 'text-(--primary-foreground)' : 'text-(--subtle-foreground)';
29
+ }
30
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, deps: [], target: i0.ɵɵFactoryTarget.Component });
31
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.7", type: UiFilterChips, isStandalone: true, selector: "ui-filter-chips", inputs: { options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: true, transformFunction: null }, value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null } }, outputs: { value: "valueChange" }, host: { attributes: { "role": "group" }, properties: { "attr.aria-label": "ariaLabel()" }, classAttribute: "flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0" }, ngImport: i0, template: `
32
+ @for (option of options(); track option.value) {
33
+ <button
34
+ class="group/chip"
35
+ type="button"
36
+ [attr.aria-pressed]="option.value === value()"
37
+ [class]="chipClasses"
38
+ (click)="value.set(option.value)"
39
+ >
40
+ <span data-chip [class]="pillClasses(option.value)"
41
+ >{{ option.label }}
42
+ @if (option.count !== undefined && option.count !== null) {
43
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
44
+ option.count
45
+ }}</span>
46
+ }
47
+ </span>
48
+ </button>
49
+ }
50
+ `, isInline: true });
51
+ }
52
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiFilterChips, decorators: [{
53
+ type: Component,
54
+ args: [{
55
+ selector: 'ui-filter-chips',
56
+ template: `
57
+ @for (option of options(); track option.value) {
58
+ <button
59
+ class="group/chip"
60
+ type="button"
61
+ [attr.aria-pressed]="option.value === value()"
62
+ [class]="chipClasses"
63
+ (click)="value.set(option.value)"
64
+ >
65
+ <span data-chip [class]="pillClasses(option.value)"
66
+ >{{ option.label }}
67
+ @if (option.count !== undefined && option.count !== null) {
68
+ &ngsp;<span class="[font-variant-numeric:var(--numeric)]" data-count [class]="countClasses(option.value)">{{
69
+ option.count
70
+ }}</span>
71
+ }
72
+ </span>
73
+ </button>
74
+ }
75
+ `,
76
+ host: {
77
+ role: 'group',
78
+ class: 'flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0',
79
+ '[attr.aria-label]': 'ariaLabel()',
80
+ },
81
+ }]
82
+ }], propDecorators: { options: [{ type: i0.Input, args: [{ isSignal: true, alias: "options", required: true }] }], value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: true }] }, { type: i0.Output, args: ["valueChange"] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }] } });
83
+
84
+ /**
85
+ * Generated bundle index. Do not edit.
86
+ */
87
+
88
+ export { UiFilterChips };
89
+ //# sourceMappingURL=joanroucoux-cairn-ui-filter-chips.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joanroucoux-cairn-ui-filter-chips.mjs","sources":["../../../projects/ui/filter-chips/filter-chips.ts","../../../projects/ui/filter-chips/joanroucoux-cairn-ui-filter-chips.ts"],"sourcesContent":["import { Component, input, model } from '@angular/core';\n\nexport type FilterChipOption = {\n value: string;\n label: string;\n count?: number | string | null;\n};\n\nconst CHIP_CLASSES =\n 'flex-none flex h-11 items-center rounded-pill border-0 bg-transparent p-0 font-[inherit] cursor-pointer transition-transform duration-(--duration-press) ease-out active:scale-(--press-scale) focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-(--ring) pointer-fine:h-8 pointer-fine:focus-visible:outline-offset-2';\n\nconst PILL_CLASSES =\n 'flex h-[34px] items-center gap-1.5 rounded-pill px-3.5 text-label font-medium whitespace-nowrap pointer-fine:h-8 pointer-fine:px-3 pointer-fine:group-hover/chip:shadow-[inset_0_0_0_1px_var(--muted-foreground)]';\n\nconst SELECTED_PILL_CLASSES = 'bg-(--primary) text-(--primary-foreground)';\n\nconst UNSELECTED_PILL_CLASSES = 'bg-(--card) text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)]';\n\n/**\n * A row of single-choice filter pills, each with an optional count, that narrows a list in place:\n * an asset class, a status. On touch it scrolls on one line, bleeding to the screen edges; with a\n * fine pointer it wraps.\n *\n * @example\n * <ui-filter-chips ariaLabel=\"Filter by class\" [options]=\"classes()\" [(value)]=\"selectedClass\" />\n */\n@Component({\n selector: 'ui-filter-chips',\n template: `\n @for (option of options(); track option.value) {\n <button\n class=\"group/chip\"\n type=\"button\"\n [attr.aria-pressed]=\"option.value === value()\"\n [class]=\"chipClasses\"\n (click)=\"value.set(option.value)\"\n >\n <span data-chip [class]=\"pillClasses(option.value)\"\n >{{ option.label }}\n @if (option.count !== undefined && option.count !== null) {\n &ngsp;<span class=\"[font-variant-numeric:var(--numeric)]\" data-count [class]=\"countClasses(option.value)\">{{\n option.count\n }}</span>\n }\n </span>\n </button>\n }\n `,\n host: {\n role: 'group',\n class:\n 'flex gap-2 overflow-x-auto [scrollbar-width:none] -mt-1 -mx-(--gutter) px-(--gutter) pointer-fine:m-0 pointer-fine:flex-wrap pointer-fine:overflow-visible pointer-fine:p-0',\n '[attr.aria-label]': 'ariaLabel()',\n },\n})\nexport class UiFilterChips {\n readonly options = input.required<FilterChipOption[]>();\n readonly value = model.required<string>();\n readonly ariaLabel = input.required<string>();\n\n protected readonly chipClasses = CHIP_CLASSES;\n\n protected pillClasses(optionValue: string): string {\n return `${PILL_CLASSES} ${optionValue === this.value() ? SELECTED_PILL_CLASSES : UNSELECTED_PILL_CLASSES}`;\n }\n\n protected countClasses(optionValue: string): string {\n return optionValue === this.value() ? 'text-(--primary-foreground)' : 'text-(--subtle-foreground)';\n }\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":";;;AAQA,MAAM,YAAY,GAChB,oVAAoV;AAEtV,MAAM,YAAY,GAChB,mNAAmN;AAErN,MAAM,qBAAqB,GAAG,4CAA4C;AAE1E,MAAM,uBAAuB,GAAG,wEAAwE;AAExG;;;;;;;AAOG;MA8BU,aAAa,CAAA;IACf,OAAO,GAAG,KAAK,CAAC,QAAQ;gFAAsB;IAC9C,KAAK,GAAG,KAAK,CAAC,QAAQ;8EAAU;IAChC,SAAS,GAAG,KAAK,CAAC,QAAQ;kFAAU;IAE1B,WAAW,GAAG,YAAY;AAEnC,IAAA,WAAW,CAAC,WAAmB,EAAA;AACvC,QAAA,OAAO,GAAG,YAAY,CAAA,CAAA,EAAI,WAAW,KAAK,IAAI,CAAC,KAAK,EAAE,GAAG,qBAAqB,GAAG,uBAAuB,EAAE;IAC5G;AAEU,IAAA,YAAY,CAAC,WAAmB,EAAA;AACxC,QAAA,OAAO,WAAW,KAAK,IAAI,CAAC,KAAK,EAAE,GAAG,6BAA6B,GAAG,4BAA4B;IACpG;uGAbW,aAAa,EAAA,IAAA,EAAA,EAAA,EAAA,MAAA,EAAA,EAAA,CAAA,eAAA,CAAA,SAAA,EAAA,CAAA;AAAb,IAAA,OAAA,IAAA,GAAA,EAAA,CAAA,oBAAA,CAAA,EAAA,UAAA,EAAA,QAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,aAAa,EAAA,YAAA,EAAA,IAAA,EAAA,QAAA,EAAA,iBAAA,EAAA,MAAA,EAAA,EAAA,OAAA,EAAA,EAAA,iBAAA,EAAA,SAAA,EAAA,UAAA,EAAA,SAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,KAAA,EAAA,EAAA,iBAAA,EAAA,OAAA,EAAA,UAAA,EAAA,OAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,SAAA,EAAA,EAAA,iBAAA,EAAA,WAAA,EAAA,UAAA,EAAA,WAAA,EAAA,QAAA,EAAA,IAAA,EAAA,UAAA,EAAA,IAAA,EAAA,iBAAA,EAAA,IAAA,EAAA,EAAA,EAAA,OAAA,EAAA,EAAA,KAAA,EAAA,aAAA,EAAA,EAAA,IAAA,EAAA,EAAA,UAAA,EAAA,EAAA,MAAA,EAAA,OAAA,EAAA,EAAA,UAAA,EAAA,EAAA,iBAAA,EAAA,aAAA,EAAA,EAAA,cAAA,EAAA,6KAAA,EAAA,EAAA,QAAA,EAAA,EAAA,EAAA,QAAA,EA3Bd;;;;;;;;;;;;;;;;;;;AAmBT,EAAA,CAAA,EAAA,QAAA,EAAA,IAAA,EAAA,CAAA;;2FAQU,aAAa,EAAA,UAAA,EAAA,CAAA;kBA7BzB,SAAS;AAAC,YAAA,IAAA,EAAA,CAAA;AACT,oBAAA,QAAQ,EAAE,iBAAiB;AAC3B,oBAAA,QAAQ,EAAE;;;;;;;;;;;;;;;;;;;AAmBT,EAAA,CAAA;AACD,oBAAA,IAAI,EAAE;AACJ,wBAAA,IAAI,EAAE,OAAO;AACb,wBAAA,KAAK,EACH,6KAA6K;AAC/K,wBAAA,mBAAmB,EAAE,aAAa;AACnC,qBAAA;AACF,iBAAA;;;ACtDD;;AAEG;;;;"}
@@ -0,0 +1,90 @@
1
+ import * as i0 from '@angular/core';
2
+ import { input, computed, forwardRef, Directive } from '@angular/core';
3
+ import { UI_CONTROL } from '@joanroucoux/cairn-ui/control';
4
+
5
+ /** Available control sizes. `md` is the 44/40px touch target; `lg` and `xl` are the 48px sheet and sign-in fields. */
6
+ const CONTROL_SIZES = ['sm', 'md', 'lg', 'xl'];
7
+ const CONTROL_BASE_CLASSES = 'w-full rounded-control px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-50';
8
+ const CONTROL_SURFACES = ['background', 'card'];
9
+ const CONTROL_SURFACE_CLASSES = {
10
+ background: 'bg-(--background)',
11
+ card: 'bg-(--card)',
12
+ };
13
+ const CONTROL_SIZE_CLASSES = {
14
+ sm: 'h-9',
15
+ md: 'min-h-(--row-min)',
16
+ lg: 'min-h-12 lg:min-h-(--row-min)',
17
+ xl: 'h-12 lg:h-11',
18
+ };
19
+ const SEARCH_CLASSES = '[&::-webkit-search-cancel-button]:appearance-none';
20
+ /**
21
+ * Styled native text input. A directive (not a wrapper component) so the host
22
+ * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)
23
+ * and every native attribute keep working without any plumbing.
24
+ *
25
+ * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
26
+ * it can show the message without any binding here.
27
+ *
28
+ * @example
29
+ * <input uiInput type="email" placeholder="you@example.com" />
30
+ */
31
+ class UiInput {
32
+ size = input('md', /* @ts-ignore */
33
+ ...(ngDevMode ? [{ debugName: "size" }] : /* istanbul ignore next */ []));
34
+ surface = input('background', /* @ts-ignore */
35
+ ...(ngDevMode ? [{ debugName: "surface" }] : /* istanbul ignore next */ []));
36
+ errors = input([], /* @ts-ignore */
37
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
38
+ touched = input(false, /* @ts-ignore */
39
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
40
+ classes = computed(() => `${CONTROL_BASE_CLASSES} ${SEARCH_CLASSES} ${CONTROL_SURFACE_CLASSES[this.surface()]} ${CONTROL_SIZE_CLASSES[this.size()]}`, /* @ts-ignore */
41
+ ...(ngDevMode ? [{ debugName: "classes" }] : /* istanbul ignore next */ []));
42
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, deps: [], target: i0.ɵɵFactoryTarget.Directive });
43
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiInput, isStandalone: true, selector: "input[uiInput]", inputs: { size: { classPropertyName: "size", publicName: "size", isSignal: true, isRequired: false, transformFunction: null }, surface: { classPropertyName: "surface", publicName: "surface", isSignal: true, isRequired: false, transformFunction: null }, errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class": "classes()" } }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }], ngImport: i0 });
44
+ }
45
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiInput, decorators: [{
46
+ type: Directive,
47
+ args: [{
48
+ selector: 'input[uiInput]',
49
+ host: {
50
+ '[class]': 'classes()',
51
+ },
52
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }],
53
+ }]
54
+ }], propDecorators: { size: [{ type: i0.Input, args: [{ isSignal: true, alias: "size", required: false }] }], surface: [{ type: i0.Input, args: [{ isSignal: true, alias: "surface", required: false }] }], errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
55
+ /**
56
+ * Styled native textarea. Lives beside the input because it shares every one of its
57
+ * classes; it takes no size, since a textarea grows from a floor instead of sitting
58
+ * at a fixed height.
59
+ *
60
+ * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around
61
+ * it can show the message without any binding here.
62
+ *
63
+ * @example
64
+ * <textarea uiTextarea maxlength="280"></textarea>
65
+ */
66
+ class UiTextarea {
67
+ errors = input([], /* @ts-ignore */
68
+ ...(ngDevMode ? [{ debugName: "errors" }] : /* istanbul ignore next */ []));
69
+ touched = input(false, /* @ts-ignore */
70
+ ...(ngDevMode ? [{ debugName: "touched" }] : /* istanbul ignore next */ []));
71
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, deps: [], target: i0.ɵɵFactoryTarget.Directive });
72
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.7", type: UiTextarea, isStandalone: true, selector: "textarea[uiTextarea]", inputs: { errors: { classPropertyName: "errors", publicName: "errors", isSignal: true, isRequired: false, transformFunction: null }, touched: { classPropertyName: "touched", publicName: "touched", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "w-full rounded-control px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-50 bg-(--background) min-h-24 py-2" }, providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }], ngImport: i0 });
73
+ }
74
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.7", ngImport: i0, type: UiTextarea, decorators: [{
75
+ type: Directive,
76
+ args: [{
77
+ selector: 'textarea[uiTextarea]',
78
+ host: {
79
+ class: `${CONTROL_BASE_CLASSES} bg-(--background) min-h-24 py-2`,
80
+ },
81
+ providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }],
82
+ }]
83
+ }], propDecorators: { errors: [{ type: i0.Input, args: [{ isSignal: true, alias: "errors", required: false }] }], touched: [{ type: i0.Input, args: [{ isSignal: true, alias: "touched", required: false }] }] } });
84
+
85
+ /**
86
+ * Generated bundle index. Do not edit.
87
+ */
88
+
89
+ export { CONTROL_BASE_CLASSES, CONTROL_SIZES, CONTROL_SIZE_CLASSES, CONTROL_SURFACES, CONTROL_SURFACE_CLASSES, UiInput, UiTextarea };
90
+ //# sourceMappingURL=joanroucoux-cairn-ui-input.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"joanroucoux-cairn-ui-input.mjs","sources":["../../../projects/ui/input/input.ts","../../../projects/ui/input/joanroucoux-cairn-ui-input.ts"],"sourcesContent":["import { Directive, computed, forwardRef, input } from '@angular/core';\n\nimport { UI_CONTROL, type UiControl, type UiControlError } from '@joanroucoux/cairn-ui/control';\n\n/** Available control sizes. `md` is the 44/40px touch target; `lg` and `xl` are the 48px sheet and sign-in fields. */\nexport const CONTROL_SIZES = ['sm', 'md', 'lg', 'xl'] as const;\nexport type ControlSize = (typeof CONTROL_SIZES)[number];\n\nexport const CONTROL_BASE_CLASSES =\n 'w-full rounded-control px-3 text-body text-(--foreground) shadow-[inset_0_0_0_1px_var(--border)] placeholder:text-(--muted-foreground) focus-visible:shadow-[inset_0_0_0_2px_var(--ring)] aria-invalid:shadow-[inset_0_0_0_2px_var(--negative)] disabled:cursor-not-allowed disabled:opacity-50';\n\nexport const CONTROL_SURFACES = ['background', 'card'] as const;\nexport type ControlSurface = (typeof CONTROL_SURFACES)[number];\n\nexport const CONTROL_SURFACE_CLASSES: Record<ControlSurface, string> = {\n background: 'bg-(--background)',\n card: 'bg-(--card)',\n};\n\nexport const CONTROL_SIZE_CLASSES: Record<ControlSize, string> = {\n sm: 'h-9',\n md: 'min-h-(--row-min)',\n lg: 'min-h-12 lg:min-h-(--row-min)',\n xl: 'h-12 lg:h-11',\n};\n\nconst SEARCH_CLASSES = '[&::-webkit-search-cancel-button]:appearance-none';\n\n/**\n * Styled native text input. A directive (not a wrapper component) so the host\n * stays a plain <input>: every forms flavor (ngModel, reactive, signal forms)\n * and every native attribute keep working without any plumbing.\n *\n * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around\n * it can show the message without any binding here.\n *\n * @example\n * <input uiInput type=\"email\" placeholder=\"you@example.com\" />\n */\n@Directive({\n selector: 'input[uiInput]',\n host: {\n '[class]': 'classes()',\n },\n providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiInput) }],\n})\nexport class UiInput implements UiControl {\n readonly size = input<ControlSize>('md');\n readonly surface = input<ControlSurface>('background');\n readonly errors = input<readonly UiControlError[]>([]);\n readonly touched = input(false);\n\n protected readonly classes = computed(\n () =>\n `${CONTROL_BASE_CLASSES} ${SEARCH_CLASSES} ${CONTROL_SURFACE_CLASSES[this.surface()]} ${CONTROL_SIZE_CLASSES[this.size()]}`,\n );\n}\n\n/**\n * Styled native textarea. Lives beside the input because it shares every one of its\n * classes; it takes no size, since a textarea grows from a floor instead of sitting\n * at a fixed height.\n *\n * Declares the `errors` and `touched` inputs Angular's `[formField]` fills, so a `ui-field` around\n * it can show the message without any binding here.\n *\n * @example\n * <textarea uiTextarea maxlength=\"280\"></textarea>\n */\n@Directive({\n selector: 'textarea[uiTextarea]',\n host: {\n class: `${CONTROL_BASE_CLASSES} bg-(--background) min-h-24 py-2`,\n },\n providers: [{ provide: UI_CONTROL, useExisting: forwardRef(() => UiTextarea) }],\n})\nexport class UiTextarea implements UiControl {\n readonly errors = input<readonly UiControlError[]>([]);\n readonly touched = input(false);\n}\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './index';\n"],"names":[],"mappings":"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"}