@ethlete/components 1.0.0-next.77 → 1.0.0-next.78

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 (78) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/design/config.json +19 -0
  3. package/design/form-field.ts +40 -1
  4. package/design/icon-set.ts +19 -0
  5. package/design/icon.ts +9 -0
  6. package/design/sdk/components/button/button.ts +39 -0
  7. package/design/sdk/components/form-field/form-field.ts +100 -0
  8. package/design/sdk/components/select/select.css +5 -0
  9. package/design/sdk/components/select/select.ts +16 -0
  10. package/design/sdk/components/select/shared/select-part.ts +60 -0
  11. package/design/sdk/fixtures.ts +0 -0
  12. package/design/sdk/foundations/colors/colors.css +528 -0
  13. package/design/sdk/foundations/colors/colors.ts +188 -0
  14. package/design/sdk/foundations/colors/overrides.json +19 -0
  15. package/design/sdk/foundations/colors/shared/contrast.ts +120 -0
  16. package/design/select.ts +113 -28
  17. package/design/tree.ts +106 -0
  18. package/fesm2022/ethlete-components.mjs +15 -15
  19. package/fesm2022/ethlete-components.mjs.map +1 -1
  20. package/package.json +1 -1
  21. package/src/lib/forms/form-field/form-field-text-shell-styles.component.css +24 -16
  22. package/src/lib/forms/select/select-option-styles.component.css +3 -2
  23. package/src/lib/icon/headless/align-icons.ts +30 -0
  24. package/src/lib/icon/headless/arrow-out-up-right-icon.ts +17 -0
  25. package/src/lib/icon/headless/arrow-right-icon.ts +23 -0
  26. package/src/lib/icon/headless/arrow-up-icon.ts +30 -0
  27. package/src/lib/icon/headless/arrows-left-right-icon.ts +31 -0
  28. package/src/lib/icon/headless/bold-icon.ts +11 -0
  29. package/src/lib/icon/headless/calendar-icon.ts +21 -0
  30. package/src/lib/icon/headless/check-icon.ts +14 -0
  31. package/src/lib/icon/headless/chevron-icon.ts +14 -0
  32. package/src/lib/icon/headless/circle-check-icon.ts +14 -0
  33. package/src/lib/icon/headless/circle-info-icon.ts +14 -0
  34. package/src/lib/icon/headless/clipboard-check-icon.ts +28 -0
  35. package/src/lib/icon/headless/clock-icon.ts +21 -0
  36. package/src/lib/icon/headless/code-block-icon.ts +15 -0
  37. package/src/lib/icon/headless/code-icon.ts +14 -0
  38. package/src/lib/icon/headless/ellipsis-icon.ts +13 -0
  39. package/src/lib/icon/headless/ellipsis-vertical-icon.ts +13 -0
  40. package/src/lib/icon/headless/eye-icons.ts +27 -0
  41. package/src/lib/icon/headless/eyedropper-icon.ts +22 -0
  42. package/src/lib/icon/headless/file-icon.ts +16 -0
  43. package/src/lib/icon/headless/filter-icon.ts +11 -0
  44. package/src/lib/icon/headless/floppy-disk-icon.ts +27 -0
  45. package/src/lib/icon/headless/focus-frame-icon.ts +16 -0
  46. package/src/lib/icon/headless/grid-2x2-icon.ts +17 -0
  47. package/src/lib/icon/headless/heading-1-icon.ts +11 -0
  48. package/src/lib/icon/headless/heading-2-icon.ts +11 -0
  49. package/src/lib/icon/headless/heading-3-icon.ts +11 -0
  50. package/src/lib/icon/headless/icon-definition.ts +14 -0
  51. package/src/lib/icon/headless/image-icon.ts +14 -0
  52. package/src/lib/icon/headless/italic-icon.ts +19 -0
  53. package/src/lib/icon/headless/link-icon.ts +17 -0
  54. package/src/lib/icon/headless/list-bulleted-icon.ts +14 -0
  55. package/src/lib/icon/headless/list-numbered-icon.ts +14 -0
  56. package/src/lib/icon/headless/location-icon.ts +21 -0
  57. package/src/lib/icon/headless/lock-icon.ts +14 -0
  58. package/src/lib/icon/headless/minus-icon.ts +14 -0
  59. package/src/lib/icon/headless/paragraph-icon.ts +11 -0
  60. package/src/lib/icon/headless/pause-icon.ts +11 -0
  61. package/src/lib/icon/headless/pencil-icon.ts +21 -0
  62. package/src/lib/icon/headless/play-icon.ts +10 -0
  63. package/src/lib/icon/headless/plus-icon.ts +21 -0
  64. package/src/lib/icon/headless/quote-icon.ts +15 -0
  65. package/src/lib/icon/headless/rotate-right-icon.ts +16 -0
  66. package/src/lib/icon/headless/star-icon.ts +14 -0
  67. package/src/lib/icon/headless/strikethrough-icon.ts +18 -0
  68. package/src/lib/icon/headless/table-icon.ts +15 -0
  69. package/src/lib/icon/headless/thumbtack-icon.ts +18 -0
  70. package/src/lib/icon/headless/times-icon.ts +14 -0
  71. package/src/lib/icon/headless/trash-icon.ts +24 -0
  72. package/src/lib/icon/headless/triangle-exclamation-icon.ts +14 -0
  73. package/src/lib/icon/headless/trophy-icon.ts +24 -0
  74. package/src/lib/icon/headless/underline-icon.ts +12 -0
  75. package/src/lib/icon/headless/undo-redo-icons.ts +24 -0
  76. package/src/lib/icon/headless/upload-icon.ts +16 -0
  77. package/src/lib/tree/tree.component.css +2 -2
  78. package/types/ethlete-components.d.ts +52 -52
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.0-next.78
4
+
5
+ ### Patch Changes
6
+
7
+ - Design: add a `textarea()` control mock to `@ethlete/components/design/form-field`, drawn like `input()`.
8
+
3
9
  ## 1.0.0-next.77
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1,19 @@
1
+ {
2
+ "products": {
3
+ "sdk": {
4
+ "tokens": [
5
+ "foundations/colors"
6
+ ],
7
+ "presets": {
8
+ "phone": {
9
+ "width": 390,
10
+ "height": 844
11
+ },
12
+ "desktop": {
13
+ "width": 1440,
14
+ "height": 900
15
+ }
16
+ }
17
+ }
18
+ }
19
+ }
@@ -5,6 +5,8 @@ import suffixOverlap from '../src/lib/forms/form-field/form-field-suffix-overlap
5
5
  import label from '../src/lib/forms/form-field/headless/label.directive.css?inline';
6
6
  import hint from '../src/lib/forms/form-field/hint.component.css?inline';
7
7
  import inputBase from '../src/lib/forms/input/input.component.css?inline';
8
+ import textareaBase from '../src/lib/forms/textarea/textarea.component.css?inline';
9
+ import textareaAutosize from '../src/lib/forms/textarea/textarea-autosize-styles.component.css?inline';
8
10
  import iconStyles from '../src/lib/icon/headless/icon-styles.component.css?inline';
9
11
 
10
12
  /** The field state a control draws itself from. */
@@ -75,7 +77,16 @@ export type InputMockOptions = {
75
77
  textAlign?: 'start' | 'center' | 'end';
76
78
  };
77
79
 
78
- /** The stylesheets `formField()` and `input()` draw with: the real `et-form-field` CSS. */
80
+ export type TextareaMockOptions = {
81
+ value?: string;
82
+ placeholder?: string;
83
+ rows?: number;
84
+ /** Grows with its content, from `minRows` (default `rows`). On by default, like the real textarea. */
85
+ autosize?: boolean;
86
+ minRows?: number;
87
+ };
88
+
89
+ /** The stylesheets `formField()`, `input()` and `textarea()` draw with: the real `et-form-field` CSS. */
79
90
  export const formFieldStyles = /* @__PURE__ */ [
80
91
  interactiveStyles,
81
92
  iconStyles,
@@ -85,6 +96,8 @@ export const formFieldStyles = /* @__PURE__ */ [
85
96
  label,
86
97
  hint,
87
98
  inputBase,
99
+ textareaBase,
100
+ textareaAutosize,
88
101
  `.et-design-panel-anchor { position: relative; }
89
102
  .et-design-panel { position: absolute; inset-inline: 0; inset-block-start: calc(100% + 0.3rem); z-index: 1; }`,
90
103
  ].join('\n');
@@ -114,6 +127,32 @@ export const input = (options: InputMockOptions = {}): FormFieldControlMock => (
114
127
  },
115
128
  });
116
129
 
130
+ /** Draws `<et-textarea>` as a control for `formField()`. */
131
+ export const textarea = (options: TextareaMockOptions = {}): FormFieldControlMock => ({
132
+ controlType: 'textarea',
133
+ hasValue: !!options.value,
134
+ html: (state) => {
135
+ const rows = options.rows ?? 3;
136
+ const autosize = options.autosize ?? true;
137
+ const attributes = attributesOf([
138
+ 'class="et-textarea-native"',
139
+ `rows="${rows}"`,
140
+ state.disabled && 'disabled',
141
+ state.readonly && 'readonly',
142
+ state.required && 'required',
143
+ options.placeholder && `placeholder="${options.placeholder}"`,
144
+ state.invalid && 'aria-invalid="true"',
145
+ state.describedBy && `aria-describedby="${state.describedBy}"`,
146
+ state.labelId && `aria-labelledby="${state.labelId}"`,
147
+ `data-resize="${autosize ? 'none' : 'vertical'}"`,
148
+ autosize && 'data-et-textarea-autosize=""',
149
+ autosize && `style="--et-textarea-min-rows: ${options.minRows ?? rows}"`,
150
+ ]);
151
+
152
+ return `<et-textarea class="et-color-interactive et-textarea"><textarea ${attributes}>${options.value ?? ''}</textarea></et-textarea>`;
153
+ },
154
+ });
155
+
117
156
  /** Draws `<et-form-field>` with a control, its label, affixes and messages, as the component renders them. */
118
157
  export const formField = (options: FormFieldMockOptions) => {
119
158
  const control: FormFieldControlMock =
@@ -0,0 +1,19 @@
1
+ /// <reference types="vite/client" />
2
+ import { IconDefinition } from '../src/lib/icon/headless/icon-definition';
3
+
4
+ const modules = /* @__PURE__ */ import.meta.glob<Record<string, unknown>>(
5
+ ['../src/lib/icon/headless/*-icon.ts', '../src/lib/icon/headless/*-icons.ts'],
6
+ { eager: true },
7
+ );
8
+
9
+ const isIconDefinition = (value: unknown): value is IconDefinition =>
10
+ !!value &&
11
+ typeof value === 'object' &&
12
+ typeof (value as IconDefinition).name === 'string' &&
13
+ typeof (value as IconDefinition).data === 'string';
14
+
15
+ /** Every icon the SDK ships, read from the icon source files and sorted by name. */
16
+ export const SDK_ICONS: readonly IconDefinition[] = /* @__PURE__ */ Object.values(modules)
17
+ .flatMap((module) => Object.values(module))
18
+ .filter(isIconDefinition)
19
+ .sort((a, b) => a.name.localeCompare(b.name));
package/design/icon.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { IconDefinition } from '../src/lib/icon/headless/icon-definition';
2
+
3
+ /** Draws `<i etIcon>` as the icon directive renders it: the icon's classes around its trimmed SVG. */
4
+ export const iconMarkup = (icon: IconDefinition, className?: string) => {
5
+ const base = `et-icon et-icon--${icon.name}`;
6
+ const classes = [className, base, icon.variant ? `${base}--${icon.variant}` : undefined].filter(Boolean).join(' ');
7
+
8
+ return `<i etIcon="${icon.name}" aria-hidden="true" class="${classes}">${icon.data.trim()}</i>`;
9
+ };
@@ -0,0 +1,39 @@
1
+ import { button, buttonStyles, ButtonMockOptions } from '@ethlete/components/design/button';
2
+ import { definePart, drawing, forceInteraction, interactionRows } from '@design-system';
3
+
4
+ type ButtonInteraction = 'hover' | 'active' | 'focus-visible';
5
+
6
+ type Props = ButtonMockOptions & { interaction?: ButtonInteraction };
7
+
8
+ const control = ({ interaction, ...options }: Props) => {
9
+ const markup = button(options).replace(/^<(button|a) /, '<$1 data-et-control="button" data-can-animate ');
10
+
11
+ return interaction ? forceInteraction(markup, interaction) : markup;
12
+ };
13
+
14
+ export const { states, grid } = interactionRows<
15
+ Props,
16
+ 'filled' | 'outline' | 'tonal' | 'transparent' | 'pressed' | 'danger' | 'small' | 'disabled',
17
+ ButtonInteraction
18
+ >(
19
+ {
20
+ filled: { label: 'Save', variant: 'filled' },
21
+ outline: { label: 'Cancel', variant: 'outline' },
22
+ tonal: { label: 'Filter', variant: 'tonal' },
23
+ transparent: { label: 'More', variant: 'transparent' },
24
+ pressed: { label: 'Grid', variant: 'outline', pressed: true },
25
+ danger: { label: 'Delete', variant: 'filled', color: 'danger' },
26
+ small: { label: 'Save', variant: 'filled', size: 'sm' },
27
+ disabled: { label: 'Save', variant: 'filled', disabled: true },
28
+ },
29
+ { interactions: ['hover', 'active', 'focus-visible'], force: (props, interaction) => ({ ...props, interaction }) },
30
+ );
31
+
32
+ export default definePart({
33
+ note: 'The SDK button, drawn by its mock from @ethlete/components/design.',
34
+ madeBy: ['claude-opus-5-5'],
35
+ states,
36
+ grid,
37
+ controls: { button: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true } },
38
+ render: (props: Props) => drawing({ body: control(props), styles: buttonStyles }),
39
+ });
@@ -0,0 +1,100 @@
1
+ import {
2
+ formField,
3
+ formFieldStyles,
4
+ input,
5
+ textarea,
6
+ type FormFieldMockOptions,
7
+ } from '@ethlete/components/design/form-field';
8
+ import { definePart, drawing, interactionRows } from '@design-system';
9
+
10
+ type FieldInteraction = 'hover' | 'focus-visible';
11
+
12
+ type FieldControl = 'input' | 'textarea';
13
+
14
+ type Axes = Pick<FormFieldMockOptions, 'appearance' | 'fill' | 'labelMode' | 'size'>;
15
+
16
+ type Props = Axes & {
17
+ control?: FieldControl;
18
+ value?: string;
19
+ hint?: string;
20
+ error?: string;
21
+ focused?: boolean;
22
+ disabled?: boolean;
23
+ interaction?: FieldInteraction;
24
+ };
25
+
26
+ const multiLine = 'Ada Lovelace\nAnalytical Engine notes';
27
+
28
+ const axisRow: Props = { value: 'Ada Lovelace' };
29
+
30
+ const AXES = {
31
+ appearance: ['box', 'underline'],
32
+ fill: ['transparent', 'filled'],
33
+ labelMode: ['static', 'inline', 'floating-inside', 'floating-outside'],
34
+ size: ['sm', 'md', 'lg'],
35
+ } as const satisfies { [K in keyof Axes]-?: readonly NonNullable<Axes[K]>[] };
36
+
37
+ const axisRows = Object.fromEntries(
38
+ Object.entries(AXES).flatMap(([axis, values]) =>
39
+ values.map((value) => [`${axis}-${value}`, { ...axisRow, [axis]: value }]),
40
+ ),
41
+ ) as Record<string, Props>;
42
+
43
+ const { states, grid } = interactionRows(
44
+ {
45
+ empty: { hint: 'Shown on your profile' },
46
+ filled: { value: 'Ada Lovelace' },
47
+ error: { error: 'This field is required' },
48
+ 'textarea-empty': { control: 'textarea', hint: 'Shown on your profile' },
49
+ 'textarea-filled': { control: 'textarea', value: multiLine },
50
+ 'textarea-error': { control: 'textarea', error: 'This field is required' },
51
+ ...axisRows,
52
+ } satisfies Record<string, Props>,
53
+ {
54
+ interactions: ['hover', 'focus-visible'],
55
+ force: (props: Props, interaction: FieldInteraction): Props => ({ ...props, interaction }),
56
+ },
57
+ );
58
+
59
+ const markControl = (markup: string, control: FieldControl, interaction: FieldInteraction | undefined) => {
60
+ const frame = ['data-et-control-part', interaction && `data-et-force="${interaction}"`].filter(Boolean);
61
+ const native = control === 'input' ? '<input class="et-input-native"' : '<textarea class="et-textarea-native"';
62
+
63
+ return markup
64
+ .replace('<et-form-field ', `<et-form-field data-et-control="${control}" `)
65
+ .replace('<div class="et-form-field-control-frame"', `<div class="et-form-field-control-frame" ${frame.join(' ')}`)
66
+ .replace(native, native.replace(' class=', ' data-et-control-part class='));
67
+ };
68
+
69
+ export default definePart<Props>({
70
+ note: 'The SDK form field with a text input and a textarea, drawn by its mocks from @ethlete/components/design.',
71
+ madeBy: ['claude-opus-5-5'],
72
+ states: {
73
+ ...states,
74
+ focused: { value: 'Ada', focused: true },
75
+ disabled: { value: 'Ada Lovelace', disabled: true },
76
+ 'textarea-focused': { control: 'textarea', value: 'Ada', focused: true },
77
+ 'textarea-disabled': { control: 'textarea', value: multiLine, disabled: true },
78
+ },
79
+ grid,
80
+ widths: [320],
81
+ controls: {
82
+ input: { kind: 'field', input: ['pointer', 'keyboard'], disabled: true },
83
+ textarea: { kind: 'field', input: ['pointer', 'keyboard'], disabled: true },
84
+ },
85
+ render: ({ control = 'input', value, interaction, ...field }) =>
86
+ drawing({
87
+ body: markControl(
88
+ formField({
89
+ id: 'name',
90
+ label: 'Name',
91
+ errorColor: 'danger-on-light',
92
+ control: control === 'input' ? input({ value }) : textarea({ value }),
93
+ ...field,
94
+ }),
95
+ control,
96
+ interaction,
97
+ ),
98
+ styles: formFieldStyles,
99
+ }),
100
+ });
@@ -0,0 +1,5 @@
1
+ @layer components {
2
+ et-form-field:where(:not([data-readonly]):not([data-disabled])) .et-form-field-control-frame:active {
3
+ border-color: var(--et-surface-interaction-active-solid, currentColor);
4
+ }
5
+ }
@@ -0,0 +1,16 @@
1
+ import { selectStyles } from '@ethlete/components/design/select';
2
+ import { definePart, drawing } from '@design-system';
3
+ import styles from './select.css?inline';
4
+ import { grid, moreStates, selectBody, SelectProps, states } from './shared/select-part';
5
+
6
+ export default definePart<SelectProps>({
7
+ note: 'The SDK select inside a form field, drawn by its mock from @ethlete/components/design.',
8
+ madeBy: ['claude-opus-5-5', 'claude-sonnet-5-5'],
9
+ states: { ...states, ...moreStates },
10
+ grid,
11
+ widths: [320],
12
+ controls: {
13
+ select: { kind: 'select', input: ['pointer', 'keyboard'], disabled: true, surfaces: { open: 'open' } },
14
+ },
15
+ render: (props) => drawing({ body: selectBody(props), styles: [selectStyles, styles].join('\n') }),
16
+ });
@@ -0,0 +1,60 @@
1
+ import { formField } from '@ethlete/components/design/form-field';
2
+ import { select } from '@ethlete/components/design/select';
3
+ import { interactionRows } from '@design-system';
4
+
5
+ type FieldInteraction = 'hover' | 'active' | 'focus-visible';
6
+
7
+ export type SelectProps = {
8
+ value?: string;
9
+ open?: boolean;
10
+ active?: string;
11
+ pointerActive?: boolean;
12
+ disabled?: boolean;
13
+ interaction?: FieldInteraction;
14
+ };
15
+
16
+ const OPTIONS = [
17
+ { value: 'drafts', label: 'Drafts' },
18
+ { value: 'review', label: 'In review' },
19
+ { value: 'merged', label: 'Merged' },
20
+ ];
21
+
22
+ const markControl = (markup: string, { interaction, pointerActive }: SelectProps) => {
23
+ const frame = ['data-et-control-part', interaction && `data-et-force="${interaction}"`].filter(Boolean);
24
+ const marked = markup
25
+ .replace('<et-form-field ', '<et-form-field data-et-control="select" ')
26
+ .replace('<div class="et-form-field-control-frame"', `<div class="et-form-field-control-frame" ${frame.join(' ')}`)
27
+ .replace('class="et-select-trigger"', 'data-et-control-part class="et-select-trigger"')
28
+ .replace('<et-select-panel ', '<et-select-panel data-et-surface="select.open" ');
29
+
30
+ return pointerActive
31
+ ? marked.replace('data-active-source="keyboard"', 'data-active-source="pointer" data-et-force="hover"')
32
+ : marked;
33
+ };
34
+
35
+ export const { states, grid } = interactionRows<SelectProps, 'placeholder' | 'selected', FieldInteraction>(
36
+ {
37
+ placeholder: {},
38
+ selected: { value: 'review' },
39
+ },
40
+ { interactions: ['hover', 'active', 'focus-visible'], force: (props, interaction) => ({ ...props, interaction }) },
41
+ );
42
+
43
+ export const moreStates = {
44
+ disabled: { value: 'review', disabled: true },
45
+ open: { value: 'review', open: true, active: 'merged' },
46
+ 'open-option-hover': { value: 'review', open: true, active: 'drafts', pointerActive: true },
47
+ } satisfies Record<string, SelectProps>;
48
+
49
+ export const selectBody = (props: SelectProps) =>
50
+ markControl(
51
+ formField({
52
+ id: 'status',
53
+ label: 'Status',
54
+ errorColor: 'danger',
55
+ open: props.open,
56
+ disabled: props.disabled,
57
+ control: select({ options: OPTIONS, value: props.value, active: props.active, placeholder: 'Pick a status' }),
58
+ }),
59
+ props,
60
+ );
File without changes