@returnless/focus-ui 0.0.1 → 0.0.3

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 (186) hide show
  1. package/dist/focus-ui.js +10851 -0
  2. package/dist/focus-ui.umd.cjs +26 -0
  3. package/dist/style.css +1 -0
  4. package/package.json +13 -8
  5. package/src/build-utils/generate-component-meta.ts +5 -1
  6. package/src/build-utils/update-component-list.ts +1 -1
  7. package/src/components/Accordion/AccordionContent.vue +34 -5
  8. package/src/components/Accordion/AccordionItem.vue +5 -2
  9. package/src/components/Accordion/AccordionTrigger.vue +5 -2
  10. package/src/components/Accordion/README.md +1 -1
  11. package/src/components/ActionList/ActionList.vue +9 -0
  12. package/src/components/ActionList/ActionListBody.vue +11 -0
  13. package/src/components/ActionList/ActionListItem.vue +37 -0
  14. package/src/components/ActionList/ActionListSection.vue +7 -0
  15. package/src/components/ActionList/ActionListTrigger.vue +9 -0
  16. package/src/components/ActionList/README.md +113 -0
  17. package/src/components/ActionList/index.ts +5 -0
  18. package/src/components/Alert/Alert.vue +23 -10
  19. package/src/components/Alert/AlertDescription.vue +13 -1
  20. package/src/components/Alert/AlertTitle.vue +1 -1
  21. package/src/components/Alert/DismissableAlertButton.vue +6 -4
  22. package/src/components/Alert/README.md +31 -2
  23. package/src/components/Alert/index.ts +2 -0
  24. package/src/components/Alert/types.ts +1 -0
  25. package/src/components/AlertDialog/AlertDialog.vue +10 -1
  26. package/src/components/AlertDialog/AlertDialogActionButton.vue +9 -2
  27. package/src/components/AlertDialog/AlertDialogCancelButton.vue +1 -1
  28. package/src/components/AlertDialog/AlertDialogDescription.vue +7 -1
  29. package/src/components/AlertDialog/AlertDialogTitle.vue +11 -3
  30. package/src/components/AlertDialog/README.md +15 -16
  31. package/src/components/AspectRatio/AspectRatio.vue +19 -0
  32. package/src/components/AspectRatio/README.md +36 -0
  33. package/src/components/AspectRatio/index.ts +1 -0
  34. package/src/components/Avatar/Avatar.vue +57 -13
  35. package/src/components/Avatar/README.md +3 -9
  36. package/src/components/Badge/Badge.vue +1 -1
  37. package/src/components/Badge/README.md +9 -9
  38. package/src/components/BarChart/BarChart.vue +80 -0
  39. package/src/components/{MetricCard/MetricCardHeader.vue → BarChart/BarChartContainer.vue} +1 -1
  40. package/src/components/BarChart/BarChartStacked.vue +93 -0
  41. package/src/components/BarChart/README.md +83 -0
  42. package/src/components/BarChart/index.ts +3 -0
  43. package/src/components/Breadcrumbs/Breadcrumb.vue +7 -0
  44. package/src/components/Breadcrumbs/BreadcrumbEllipsis.vue +12 -0
  45. package/src/components/{MetricCard/MetricCardValue.vue → Breadcrumbs/BreadcrumbItem.vue} +2 -2
  46. package/src/components/Breadcrumbs/BreadcrumbLink.vue +13 -0
  47. package/src/components/Breadcrumbs/BreadcrumbList.vue +8 -0
  48. package/src/components/Breadcrumbs/BreadcrumbPage.vue +13 -0
  49. package/src/components/Breadcrumbs/BreadcrumbSeparator.vue +12 -0
  50. package/src/components/Breadcrumbs/README.md +91 -0
  51. package/src/components/Breadcrumbs/index.ts +7 -0
  52. package/src/components/Button/Button.vue +53 -41
  53. package/src/components/Button/ButtonContent.vue +1 -1
  54. package/src/components/Button/ButtonIcon.vue +28 -3
  55. package/src/components/Button/README.md +32 -29
  56. package/src/components/Button/index.ts +2 -0
  57. package/src/components/Button/types.ts +30 -0
  58. package/src/components/ButtonGroup/README.md +1 -1
  59. package/src/components/Card/CardHelp.vue +23 -0
  60. package/src/components/Card/CardSection.vue +17 -2
  61. package/src/components/Card/CardTitle.vue +6 -3
  62. package/src/components/Card/README.md +97 -10
  63. package/src/components/Card/index.ts +2 -1
  64. package/src/components/Checkbox/Checkbox.vue +29 -5
  65. package/src/components/Checkbox/README.md +34 -5
  66. package/src/components/DatePicker/DatePicker.vue +7 -27
  67. package/src/components/DatePicker/README.md +1 -1
  68. package/src/components/DescriptionList/DescriptionList.vue +1 -1
  69. package/src/components/DescriptionList/DescriptionListItem.vue +1 -1
  70. package/src/components/DescriptionList/README.md +2 -2
  71. package/src/components/Dialog/README.md +2 -0
  72. package/src/components/Dialog/index.ts +0 -0
  73. package/src/components/DropZone/DropZone.vue +105 -0
  74. package/src/components/DropZone/README.md +48 -0
  75. package/src/components/DropZone/index.ts +1 -0
  76. package/src/components/EmptyState/README.md +1 -1
  77. package/src/components/Feed/FeedItem.vue +4 -1
  78. package/src/components/Feed/FeedItemBlock.vue +4 -1
  79. package/src/components/Feed/README.md +1 -1
  80. package/src/components/FileUploadButton/FileUploadButton.vue +62 -0
  81. package/src/components/FileUploadButton/index.ts +1 -0
  82. package/src/components/Form/Form.vue +7 -2
  83. package/src/components/Form/README.md +1 -1
  84. package/src/components/FormLayout/FormLayout.vue +20 -2
  85. package/src/components/FormLayout/README.md +39 -1
  86. package/src/components/Heading/Heading.vue +32 -0
  87. package/src/components/Heading/index.ts +3 -0
  88. package/src/components/Heading/types.ts +3 -0
  89. package/src/components/Image/Image.vue +30 -0
  90. package/src/components/Image/index.ts +1 -0
  91. package/src/components/InertiaLink/InertiaLink.vue +11 -0
  92. package/src/components/InertiaLink/index.ts +1 -0
  93. package/src/components/InlineError/InlineError.vue +21 -0
  94. package/src/components/InlineError/README.md +63 -0
  95. package/src/components/InlineError/index.ts +1 -0
  96. package/src/components/KPICard/KPICard.vue +28 -0
  97. package/src/components/KPICard/KPICardSection.vue +30 -0
  98. package/src/components/KPICard/README.md +124 -0
  99. package/src/components/KPICard/index.ts +2 -0
  100. package/src/components/Legend/Legend.vue +7 -0
  101. package/src/components/Legend/LegendItem.vue +34 -0
  102. package/src/components/Legend/README.md +32 -0
  103. package/src/components/Legend/index.ts +2 -0
  104. package/src/components/Link/Link.vue +4 -4
  105. package/src/components/Link/README.md +1 -1
  106. package/src/components/Navigation/Navigation.vue +2 -2
  107. package/src/components/Navigation/NavigationItem.vue +14 -10
  108. package/src/components/Navigation/NavigationSecondarySection.vue +12 -0
  109. package/src/components/Navigation/NavigationSection.vue +1 -1
  110. package/src/components/Navigation/README.md +10 -15
  111. package/src/components/Navigation/index.ts +1 -0
  112. package/src/components/Page/Page.vue +2 -33
  113. package/src/components/Page/PageBody.vue +36 -0
  114. package/src/components/Page/PageTitle.vue +6 -3
  115. package/src/components/Page/README.md +45 -39
  116. package/src/components/Page/index.ts +1 -0
  117. package/src/components/Pagination/README.md +1 -1
  118. package/src/components/PinInput/README.md +1 -1
  119. package/src/components/Popover/Popover.vue +18 -0
  120. package/src/components/Popover/PopoverBody.vue +11 -0
  121. package/src/components/Popover/PopoverTrigger.vue +9 -0
  122. package/src/components/Popover/README.md +34 -6
  123. package/src/components/Popover/index.ts +3 -0
  124. package/src/components/Popper/Popper.vue +91 -0
  125. package/src/components/Popper/PopperBody.vue +19 -0
  126. package/src/components/Popper/PopperTrigger.vue +14 -0
  127. package/src/components/Popper/README.md +42 -0
  128. package/src/components/Popper/index.ts +3 -0
  129. package/src/components/ProgressBar/ProgressBar.vue +24 -6
  130. package/src/components/RadioButton/README.md +1 -1
  131. package/src/components/RadioButton/RadioButton.vue +3 -2
  132. package/src/components/ResourceList/README.md +160 -0
  133. package/src/components/ResourceList/ResourceList.vue +7 -0
  134. package/src/components/ResourceList/ResourceListItem.vue +7 -0
  135. package/src/components/ResourceList/ResourceListItemContent.vue +7 -0
  136. package/src/components/ResourceList/index.ts +3 -0
  137. package/src/components/Select/README.md +1 -1
  138. package/src/components/Select/Select.vue +1 -1
  139. package/src/components/Separator/README.md +5 -1
  140. package/src/components/Separator/Separator.vue +20 -3
  141. package/src/components/Spinner/README.md +1 -1
  142. package/src/components/Spinner/Spinner.vue +10 -4
  143. package/src/components/StatusIndicator/README.md +2 -2
  144. package/src/components/StatusIndicator/StatusIndicator.vue +11 -5
  145. package/src/components/Stepper/README.md +38 -0
  146. package/src/components/Stepper/Stepper.vue +104 -0
  147. package/src/components/Stepper/index.ts +1 -0
  148. package/src/components/Tabs/README.md +1 -1
  149. package/src/components/Tabs/TabTrigger.vue +5 -4
  150. package/src/components/Tabs/Tabs.vue +4 -1
  151. package/src/components/Tag/Tag.vue +45 -0
  152. package/src/components/Tag/index.ts +1 -0
  153. package/src/components/TextField/README.md +24 -6
  154. package/src/components/TextField/TextField.vue +25 -5
  155. package/src/components/TextField/TextFieldIcon.vue +19 -0
  156. package/src/components/TextStyle/README.md +1 -1
  157. package/src/components/TextStyle/TextStyle.vue +1 -1
  158. package/src/components/Toast/DismissToastAction.vue +1 -1
  159. package/src/components/Toast/README.md +1 -1
  160. package/src/components/Toggle/README.md +1 -1
  161. package/src/components/Toggle/Toggle.vue +8 -5
  162. package/src/components/Tooltip/README.md +1 -1
  163. package/src/components/Tooltip/Tooltip.vue +15 -41
  164. package/src/components/TopBar/TopBarSearch.vue +2 -2
  165. package/src/components/index.ts +68 -12
  166. package/src/components/types.ts +5 -0
  167. package/src/composables/useTheme.ts +13 -1
  168. package/src/composables/useToastNotifications.ts +1 -1
  169. package/src/composables/useUniqueId.ts +4 -3
  170. package/src/index.css +17 -13
  171. package/src/index.ts +0 -11
  172. package/dist/focus-ui.es.js +0 -33
  173. package/dist/types/components/Accordion/Accordion.vue.d.ts +0 -32
  174. package/dist/types/components/Accordion/AccordionItem.vue.d.ts +0 -2
  175. package/dist/types/components/Accordion/index.d.ts +0 -2
  176. package/dist/types/components/index.d.ts +0 -1
  177. package/dist/types/index.d.ts +0 -7
  178. package/src/components/CategoryBar/CategoryBar.vue +0 -25
  179. package/src/components/CategoryBar/CategoryBarItem.vue +0 -34
  180. package/src/components/CategoryBar/README.md +0 -17
  181. package/src/components/CategoryBar/index.ts +0 -2
  182. package/src/components/MetricCard/MetricCard.vue +0 -11
  183. package/src/components/MetricCard/MetricCardLabel.vue +0 -9
  184. package/src/components/MetricCard/MetricCardSection.vue +0 -11
  185. package/src/components/MetricCard/README.md +0 -53
  186. package/src/components/MetricCard/index.ts +0 -5
@@ -1,6 +1,7 @@
1
1
  export { default as Card } from './Card.vue';
2
- export { default as CardSection } from './CardSection.vue';
3
2
  export { default as CardDescription } from './CardDescription.vue';
4
3
  export { default as CardFooter } from './CardFooter.vue';
5
4
  export { default as CardHeader } from './CardHeader.vue';
5
+ export { default as CardHelp } from './CardHelp.vue';
6
+ export { default as CardSection } from './CardSection.vue';
6
7
  export { default as CardTitle } from './CardTitle.vue';
@@ -1,4 +1,5 @@
1
1
  <script lang="ts" setup>
2
+ import { computed, ref } from 'vue';
2
3
  import { InputLabel } from '../InputLabel';
3
4
  import { useUniqueId, useTheme } from '../../composables';
4
5
  import { TextStyle } from '../TextStyle';
@@ -13,29 +14,52 @@ const props = withDefaults(defineProps<{
13
14
  /** The label for the checkbox. */
14
15
  label: string;
15
16
 
17
+ /** Whether the label is hidden. */
18
+ labelHidden?: boolean;
19
+
16
20
  /** The value of the checkbox. */
17
21
  value: any;
18
22
  }>(), {
19
23
  id: null,
20
24
  helpText: null,
25
+ labelHidden: false,
21
26
  });
22
27
 
23
- const model = defineModel<boolean>();
28
+ const $checkboxElement = ref<HTMLInputElement | null>(null);
29
+
30
+ const model = defineModel<boolean | boolean[]>();
24
31
 
25
32
  const elementId = props.id || useUniqueId('checkbox');
33
+
34
+ const elementIsChecked = computed((): boolean | undefined => {
35
+ if (Array.isArray(model.value)) {
36
+ return model.value.includes(props.value);
37
+ }
38
+
39
+ return model.value;
40
+ });
26
41
  </script>
27
42
 
28
43
  <template>
29
- <div class="flex items-start space-x-2">
44
+ <div
45
+ :aria-checked="elementIsChecked"
46
+ :aria-labelledby="elementId"
47
+ class="flex items-start space-x-2"
48
+ role="checkbox"
49
+ >
30
50
  <input
31
51
  :id="elementId"
52
+ ref="$checkboxElement"
32
53
  v-model="model"
33
- :value="value"
34
- class="appearance-none border-slate-400 shadow-sm border bg-white h-4 w-4 rounded text-brand-500 focus:ring-indigo-600"
35
54
  :class="useTheme('focus')"
55
+ :value="value"
56
+ class="h-4 w-4 flex-shrink-0 appearance-none rounded border border-slate-400 bg-white shadow-sm text-brand-500"
36
57
  type="checkbox"
37
58
  >
38
- <div class="-mt-[2px] space-y-1">
59
+ <div
60
+ v-if="!labelHidden"
61
+ class="-mt-[2px] space-y-1"
62
+ >
39
63
  <InputLabel
40
64
  :label="label"
41
65
  :label-for="elementId"
@@ -3,7 +3,7 @@ import { FormLayout, Checkbox } from '../../src/components';
3
3
  import api from '../component-meta/Checkbox.json';
4
4
  import { ref } from "vue";
5
5
 
6
- const test = ref([]);
6
+ const checkboxValues = ref([]);
7
7
  </script>
8
8
 
9
9
  # Checkbox
@@ -18,24 +18,53 @@ also be used as a way to have users indicate they agree to specific terms and se
18
18
  <ComponentWrapper>
19
19
  <FormLayout>
20
20
  <Checkbox
21
- v-model="test"
21
+ v-model="checkboxValues"
22
22
  value="comments"
23
23
  label="Comments"
24
24
  help-text="Get notified when someones posts a comment on a posting." />
25
25
  <Checkbox
26
- v-model="test"
26
+ v-model="checkboxValues"
27
27
  value="candidates"
28
28
  label="Candidates"
29
29
  help-text="Get notified when a candidate applies for a job." />
30
30
  <Checkbox
31
- v-model="test"
31
+ v-model="checkboxValues"
32
32
  value="offers"
33
33
  label="Offers"
34
34
  help-text="Get notified when a candidate accepts or rejects an offer." />
35
35
  </FormLayout>
36
- <pre class="mt-4">{{ test }}</pre>
36
+ <pre class="mt-4">{{ checkboxValues }}</pre>
37
37
  </ComponentWrapper>
38
38
 
39
+ ```js-vue
40
+ <script lang="ts" setup>
41
+ import { ref } from 'vue';
42
+ import { FormLayout, Checkbox } from '@returnless/focus-ui';
43
+
44
+ const checkboxValues = ref([]);
45
+ </script>
46
+
47
+ <template>
48
+ <FormLayout>
49
+ <Checkbox
50
+ v-model="checkboxValues"
51
+ value="comments"
52
+ label="Comments"
53
+ help-text="Get notified when someones posts a comment on a posting." />
54
+ <Checkbox
55
+ v-model="checkboxValues"
56
+ value="candidates"
57
+ label="Candidates"
58
+ help-text="Get notified when a candidate applies for a job." />
59
+ <Checkbox
60
+ v-model="checkboxValues"
61
+ value="offers"
62
+ label="Offers"
63
+ help-text="Get notified when a candidate accepts or rejects an offer." />
64
+ </FormLayout>
65
+ </template>
66
+ ```
67
+
39
68
  ## Best practices
40
69
 
41
70
  Checkboxes should:
@@ -9,14 +9,12 @@ type Day = {
9
9
  value: Dayjs;
10
10
  };
11
11
 
12
- const props = withDefaults(defineProps<{
13
- rangePicker?: boolean;
12
+ withDefaults(defineProps<{
13
+ locale?: string;
14
14
  }>(), {
15
- rangePicker: false,
15
+ locale: 'en',
16
16
  });
17
17
 
18
- const activeRangePick = ref<0 | 1>(0);
19
-
20
18
  function getCalendarMonthDays(input: Dayjs): Day[] {
21
19
  const startOfMonth = input.startOf('month');
22
20
  const days = [];
@@ -40,32 +38,18 @@ function getCalendarMonthDays(input: Dayjs): Day[] {
40
38
  return days;
41
39
  }
42
40
 
43
- const selectedDate = defineModel<string | string[]>({
41
+ const selectedDate = defineModel<string>({
44
42
  required: true,
45
43
  });
46
44
 
47
- const currentDate = ref(dayjs(new Date()));
45
+ const currentDate = ref(dayjs(selectedDate.value));
48
46
 
49
47
  const days = computed(() => {
50
48
  return getCalendarMonthDays(dayjs(currentDate.value));
51
49
  });
52
50
 
53
- function isRangePicker(): boolean {
54
- return props.rangePicker;
55
- }
56
-
57
51
  function selectDate(date: Day): void {
58
- if (isRangePicker()) {
59
- const temporarySelectedDate = selectedDate.value as string[];
60
-
61
- temporarySelectedDate[activeRangePick.value] = date.value.format('YYYY-MM-DD');
62
-
63
- activeRangePick.value = activeRangePick.value === 0 ? 1 : 0;
64
-
65
- selectedDate.value = temporarySelectedDate;
66
- } else {
67
- selectedDate.value = date.value.format('YYYY-MM-DD');
68
- }
52
+ selectedDate.value = date.value.format('YYYY-MM-DD');
69
53
  }
70
54
 
71
55
  function selectPreviousMonth(): void {
@@ -76,11 +60,7 @@ function selectNextMonth(): void {
76
60
  }
77
61
 
78
62
  function isSelected(date: Day): boolean {
79
- return isRangePicker()
80
- ? selectedDate.value.includes(date.value.format('YYYY-MM-DD'))
81
- : selectedDate.value === date.value.format('YYYY-MM-DD');
82
-
83
- // return dayjs(selectedDate.value).format('YYYY-MM-DD') === date.value.format('YYYY-MM-DD');
63
+ return dayjs(selectedDate.value).format('YYYY-MM-DD') === date.value.format('YYYY-MM-DD');
84
64
  }
85
65
 
86
66
  function isToday(date: Day): boolean {
@@ -24,7 +24,7 @@ selected across the interface.
24
24
 
25
25
  ```js-vue
26
26
  <script lang="ts" setup>
27
- import { DatePicker, DatePickerCard } from 'focus-ui';
27
+ import { DatePicker, DatePickerCard } from '@returnless/focus-ui';
28
28
  </script>
29
29
 
30
30
  <template>
@@ -3,7 +3,7 @@ import { provide } from 'vue';
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
5
  /** The alignment of the description list. */
6
- align: 'horizontal' | 'vertical';
6
+ align?: 'horizontal' | 'vertical';
7
7
  }>(), {
8
8
  align: 'horizontal',
9
9
  });
@@ -3,7 +3,7 @@ import { computed, inject } from 'vue';
3
3
 
4
4
  const descriptionListAlignment = inject<'horizontal' | 'vertical'>('descriptionListAlignment');
5
5
 
6
- const classList = computed(() => {
6
+ const classList = computed((): Record<string, boolean>[] => {
7
7
  return [
8
8
  { 'grid-cols-4 gap-4 py-3': descriptionListAlignment === 'horizontal' },
9
9
  { 'py-2': descriptionListAlignment === 'vertical' },
@@ -45,7 +45,7 @@ import {
45
45
  DescriptionListDescription,
46
46
  DescriptionListItem,
47
47
  DescriptionListTerm,
48
- } from 'focus-ui';
48
+ } from '@returnless/focus-ui';
49
49
  </script>
50
50
 
51
51
  <template>
@@ -100,7 +100,7 @@ import {
100
100
  DescriptionListDescription,
101
101
  DescriptionListItem,
102
102
  DescriptionListTerm,
103
- } from 'focus-ui';
103
+ } from '@returnless/focus-ui';
104
104
  </script>
105
105
 
106
106
  <template>
@@ -0,0 +1,2 @@
1
+ # Dialog
2
+
File without changes
@@ -0,0 +1,105 @@
1
+ <script lang="ts" setup>
2
+ import { useFileDialog, useDropZone } from '@vueuse/core';
3
+ import { Button } from '../Button';
4
+ import { computed, ref } from 'vue';
5
+ import { FileAccepts } from '../types';
6
+
7
+ const props = withDefaults(defineProps<{
8
+ /** The file types to accept. */
9
+ accepts: FileAccepts;
10
+
11
+ label: string;
12
+
13
+ /** The label for the action button. */
14
+ actionLabel: string;
15
+
16
+ /** Whether to accept multiple files. */
17
+ multiple?: boolean;
18
+ }>(), {
19
+ accepts: '*',
20
+ multiple: false,
21
+ });
22
+
23
+ const model = defineModel<File[]>({
24
+ default: [],
25
+ });
26
+
27
+ const dropZoneRef = ref<HTMLDivElement>();
28
+
29
+ const acceptTypes = computed(() => {
30
+ switch (props.accepts) {
31
+ case 'image':
32
+ return 'image/*';
33
+ case 'pdf':
34
+ return 'application/pdf';
35
+ default:
36
+ return '*';
37
+ }
38
+ });
39
+
40
+ const { open, onChange } = useFileDialog({
41
+ accept: acceptTypes.value,
42
+ multiple: props.multiple,
43
+ });
44
+
45
+ const { isOverDropZone } = useDropZone(dropZoneRef, {
46
+ onDrop: onFileUpload,
47
+ // specify the types of data to be received.
48
+ dataTypes: (types: readonly string[]) => {
49
+ if (props.accepts === '*') {
50
+ return true;
51
+ }
52
+
53
+ if (props.accepts === 'image') {
54
+ return types.some((type) => type.startsWith('image'));
55
+ }
56
+
57
+ return types.includes(props.accepts);
58
+ },
59
+ });
60
+
61
+ onChange((files: FileList | null): void => {
62
+ const listOfFiles: File[] = Array.from(files || []);
63
+
64
+ onFileUpload(listOfFiles);
65
+ });
66
+
67
+ function onFileUpload(files: File[] | null): void {
68
+ if (files === null) {
69
+ return;
70
+ }
71
+
72
+ model.value = props.multiple
73
+ ? [...model.value, ...files]
74
+ : [files[0]];
75
+ }
76
+
77
+ const classList = computed((): Record<string, boolean>[] => {
78
+ return [
79
+ { 'bg-blue-500': isOverDropZone.value },
80
+ ];
81
+ });
82
+ </script>
83
+
84
+ <template>
85
+ <button
86
+ ref="dropZoneRef"
87
+ :class="classList"
88
+ class="block w-full cursor-pointer rounded border border-dashed bg-white hover:border-solid hover:bg-slate-50"
89
+ @click="() => open()"
90
+ >
91
+ <span class="flex flex-col justify-center px-4 py-12 space-y-4">
92
+ <span>
93
+ <Button variant="secondary">
94
+ {{ actionLabel }}
95
+ </Button>
96
+ </span>
97
+ <span
98
+ v-if="label"
99
+ class="block"
100
+ >
101
+ {{ label }}
102
+ </span>
103
+ </span>
104
+ </button>
105
+ </template>
@@ -0,0 +1,48 @@
1
+ # Drop zone
2
+
3
+ The drop zone component lets users upload files by dragging and dropping the files into an area on a page, or activating
4
+ a button.
5
+
6
+ ## Usage
7
+
8
+ ## Best practices
9
+
10
+ Drop zones should:
11
+
12
+ - Inform users when the file(s) can't be uploaded.
13
+ - When possible, use validation errors on drag to detect and explain things like file size limits or file types
14
+ accepted.
15
+ - Use the banner component with a critical status to communicate errors that happen on the server.
16
+ - Provide feedback once the file(s) have been dropped and uploading begins.
17
+ - Provide a file upload button to allow users to select files for upload in traditional way.
18
+
19
+ ### Validation errors
20
+
21
+ The drop zone component validates file type by default. File types you wish to accept can be defined by editing the
22
+ `accept` property. This component also accepts custom validations using the `customValidator` property. When
23
+ validation fails, the components sets itself to error mode.
24
+
25
+ ## Content guidelines
26
+
27
+ ### Client-side validation error messages
28
+
29
+ Validation error messages should be:
30
+
31
+ - Explicit: help users understand why their file can't be uploaded and what they should change to successfully upload
32
+ their file
33
+ - In sentence case: capitalize only the first word in a message
34
+ - Concise: use simple, clear language that can be read at a glance: For example:
35
+ - `File size must be less than 20MB`
36
+ - `File type must be .pdf, .doc, or .docx`
37
+
38
+ ## Accessibility
39
+
40
+ The drop zone component builds on the native HTML `<input type="upload" />` element. It includes a visual `<button>`
41
+ as well as a drag and drop area that can receive keyboard focus.
42
+
43
+ ## Keyboard support
44
+
45
+ To upload a file with the keyboard, users can interact with the drag-and-drop region.
46
+
47
+ - To give the input keyboard focus, use the tab key (or shift + tab when tabbing backwards)
48
+ - To activate the input, use the enter/return or space keys
@@ -0,0 +1 @@
1
+ export { default as DropZone } from './DropZone.vue';
@@ -42,7 +42,7 @@ import {
42
42
  EmptyStateContent,
43
43
  EmptyStateDescription,
44
44
  EmptyStateTitle,
45
- } from 'focus-ui';
45
+ } from '@returnless/focus-ui';
46
46
  </script>
47
47
 
48
48
  <template>
@@ -2,7 +2,10 @@
2
2
  </script>
3
3
 
4
4
  <template>
5
- <li class="relative flex gap-x-2 [&:not(:last-child)]:pb-4">
5
+ <li
6
+ class="relative flex gap-x-2 [&:not(:last-child)]:pb-4"
7
+ role="listitem"
8
+ >
6
9
  <div class="absolute top-0 -bottom-0 left-0 flex w-6 justify-center">
7
10
  <div class="w-px bg-slate-200" />
8
11
  </div>
@@ -2,7 +2,10 @@
2
2
  </script>
3
3
 
4
4
  <template>
5
- <div class="flex-auto rounded border bg-white/50 px-3 py-2 text-xs leading-5 shadow-sm">
5
+ <div
6
+ class="flex-auto rounded border bg-white/50 px-3 py-2 text-xs leading-5 shadow-sm"
7
+ role="note"
8
+ >
6
9
  <slot />
7
10
  </div>
8
11
  </template>
@@ -66,7 +66,7 @@ import {
66
66
  FeedItemDateIndicator,
67
67
  FeedItemIcon,
68
68
  TextStyle,
69
- } from 'focus-ui';
69
+ } from '@returnless/focus-ui';
70
70
  </script>
71
71
 
72
72
  <template>
@@ -0,0 +1,62 @@
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue';
3
+ import { useFileDialog } from '@vueuse/core';
4
+ import { Button, ButtonProps } from '../Button';
5
+ import { FileAccepts } from '../types';
6
+
7
+ const props = withDefaults(defineProps<ButtonProps & {
8
+ /** The file types to accept. */
9
+ accepts: FileAccepts;
10
+
11
+ /** Whether to accept multiple files. */
12
+ multiple?: boolean;
13
+ }>(), {
14
+ accepts: '*',
15
+ multiple: false,
16
+ });
17
+
18
+ const model = defineModel<File[]>({
19
+ default: [],
20
+ });
21
+
22
+ const acceptTypes = computed(() => {
23
+ switch (props.accepts) {
24
+ case 'image':
25
+ return 'image/*';
26
+ case 'pdf':
27
+ return 'application/pdf';
28
+ default:
29
+ return '*';
30
+ }
31
+ });
32
+
33
+ const { open, onChange } = useFileDialog({
34
+ accept: acceptTypes.value,
35
+ multiple: props.multiple,
36
+ });
37
+
38
+ onChange((files: FileList | null): void => {
39
+ const listOfFiles: File[] = Array.from(files || []);
40
+
41
+ onFileUpload(listOfFiles);
42
+ });
43
+
44
+ function onFileUpload(files: File[] | null): void {
45
+ if (files === null) {
46
+ return;
47
+ }
48
+
49
+ model.value = props.multiple
50
+ ? [...model.value, ...files]
51
+ : [files[0]];
52
+ }
53
+ </script>
54
+
55
+ <template>
56
+ <Button
57
+ v-bind="$props"
58
+ @click="open"
59
+ >
60
+ <slot />
61
+ </Button>
62
+ </template>
@@ -0,0 +1 @@
1
+ export { default as FileUploadButton } from './FileUploadButton.vue';
@@ -2,13 +2,17 @@
2
2
  import { useUniqueId } from '../../composables';
3
3
 
4
4
  const props = withDefaults(defineProps<{
5
+ /** The aria-label attribute to be applied to the link */
6
+ accessibilityLabel?: string;
7
+
5
8
  /** The encoding type for the form. */
6
- enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain' | null;
9
+ enctype?: 'application/x-www-form-urlencoded' | 'multipart/form-data' | 'text/plain';
7
10
 
8
11
  /** The ID of the form. */
9
12
  id?: string | null;
10
13
  }>(), {
11
- enctype: null,
14
+ accessibilityLabel: undefined,
15
+ enctype: undefined,
12
16
  id: null,
13
17
  });
14
18
 
@@ -22,6 +26,7 @@ const elementId = props.id || useUniqueId('form');
22
26
  <template>
23
27
  <form
24
28
  :id="elementId"
29
+ :aria-label="accessibilityLabel"
25
30
  :enctype="enctype"
26
31
  @submit.prevent="$emit('submit')"
27
32
  >
@@ -21,7 +21,7 @@ A wrapper component that handles the submission of forms.
21
21
 
22
22
  ```js-vue
23
23
  <script lang="ts" setup>
24
- import { Form, FormLayout, TextField, Button } from 'focus-ui';
24
+ import { Form, FormLayout, TextField, Button } from '@returnless/focus-ui';
25
25
  </script>
26
26
 
27
27
  <template>
@@ -1,7 +1,25 @@
1
- <script lang="ts" setup></script>
1
+ <script lang="ts" setup>
2
+ import { computed } from 'vue';
3
+
4
+ const props = withDefaults(defineProps<{
5
+ columns: number;
6
+ }>(), {
7
+ columns: 1,
8
+ });
9
+
10
+ const classList = computed((): Record<string, boolean>[] => {
11
+ return [
12
+ { 'md:grid-cols-1': props.columns === 1 },
13
+ { 'md:grid-cols-2': props.columns === 2 },
14
+ ];
15
+ });
16
+ </script>
2
17
 
3
18
  <template>
4
- <div class="space-y-4">
19
+ <div
20
+ class="grid grid-cols-1 gap-4"
21
+ :class="classList"
22
+ >
5
23
  <slot />
6
24
  </div>
7
25
  </template>
@@ -19,7 +19,7 @@ support horizontal groups of fields.
19
19
 
20
20
  ```js-vue
21
21
  <script lang="ts" setup>
22
- import { FormLayout, TextField } from 'focus-ui';
22
+ import { FormLayout, TextField } from '@returnless/focus-ui';
23
23
  </script>
24
24
 
25
25
  <template>
@@ -30,6 +30,44 @@ import { FormLayout, TextField } from 'focus-ui';
30
30
  </template>
31
31
  ```
32
32
 
33
+ ### Horizontal form layout
34
+
35
+ <ComponentWrapper>
36
+ <FormLayout :columns="2">
37
+ <TextField label="Company name" />
38
+ <TextField label="Invoice email" />
39
+ <TextField label="Street + house number" />
40
+ <FormLayout :columns="2">
41
+ <TextField label="Postcode" />
42
+ <TextField label="City" />
43
+ </FormLayout>
44
+ <TextField label="Country" />
45
+ <TextField label="VAT number" />
46
+ <TextField label="P.O. number / reference" />
47
+ </FormLayout>
48
+ </ComponentWrapper>
49
+
50
+ ```js-vue
51
+ <script lang="ts" setup>
52
+ import { FormLayout, TextField } from '@returnless/focus-ui';
53
+ </script>
54
+
55
+ <template>
56
+ <FormLayout :columns="2">
57
+ <TextField label="Company name" />
58
+ <TextField label="Invoice email" />
59
+ <TextField label="Street + house number" />
60
+ <FormLayout :columns="2">
61
+ <TextField label="Postcode" />
62
+ <TextField label="City" />
63
+ </FormLayout>
64
+ <TextField label="Country" />
65
+ <TextField label="VAT number" />
66
+ <TextField label="P.O. number / reference" />
67
+ </FormLayout>
68
+ </template>
69
+ ```
70
+
33
71
  ## Best practices
34
72
 
35
73
  Forms should: