@threadlabs/looma 0.12.0 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -5,8 +5,13 @@
5
5
  focus ring around the input and its affixes together; the input inside drops its own through
6
6
  its custom properties, so it is still the one ui-input, with its form behaviour intact. The
7
7
  affixes are plain text beside the input, so name the input with a label that says what the
8
- whole value is. */
8
+ whole value is. The action slot takes one button at the end, inside the border, for the one
9
+ thing the field is for ("Continue" after a site's address), so it reads as the field's action
10
+ and not the form's. */
9
11
  :scope {
12
+ /* The group's own border and height stand for the input's, so the group is a lone Input's height. */
13
+ --ui-input-border-width: 0;
14
+ --ui-input-min-block-size: 0;
10
15
  --ui-input-border: transparent;
11
16
  --ui-input-border-hover: transparent;
12
17
  --ui-input-focus-border: transparent;
@@ -14,6 +19,7 @@
14
19
  --ui-input-invalid-focus-shadow: none;
15
20
  --ui-input-shadow: none;
16
21
  --ui-input-focus-shadow: none;
22
+ --ui-input-focus-outline: none;
17
23
  --ui-input-radius: 0;
18
24
  --ui-input-surface: transparent;
19
25
 
@@ -23,20 +29,25 @@
23
29
  inline-size: 100%;
24
30
  min-inline-size: 0;
25
31
  min-block-size: var(--ui-control-size-md);
26
- border: 1px solid var(--ui-border-strong);
32
+ /* The frame reads the shared control tokens with a lone Input's fallbacks, so the two match. */
33
+ border: 1px solid var(--ui-control-border, var(--ui-border-strong));
27
34
  border-radius: var(--ui-radius-md);
28
- background: var(--ui-surface-elevated);
35
+ background: var(--ui-control-surface, var(--ui-surface-elevated));
29
36
  box-shadow: var(--ui-control-inset);
30
37
  overflow: hidden;
38
+ /* A press anywhere in the frame focuses the input. */
39
+ cursor: text;
31
40
  transition: border-color var(--ui-motion-fast) var(--ui-motion-ease), box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
32
41
  }
33
42
 
34
- :scope:hover:not(:focus-within) {
35
- border-color: var(--ui-text-secondary);
43
+ :scope:hover:not(:has(input:focus, input:disabled)) {
44
+ border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
36
45
  }
37
46
 
38
- :scope:focus-within {
39
- border-color: var(--ui-accent-solid);
47
+ /* The ring shows when the field's input would show its own: when it is focus-visible, which a
48
+ text input is for a pointer as well as a keyboard. A focused action button draws its own. */
49
+ :scope:has(.field input:focus-visible) {
50
+ border-color: var(--ui-control-focus, var(--ui-accent-solid));
40
51
  box-shadow:
41
52
  var(--ui-control-inset),
42
53
  var(--ui-control-focus-halo-shadow);
@@ -46,9 +57,25 @@
46
57
  border-color: var(--ui-danger-solid);
47
58
  }
48
59
 
49
- :scope[data-ui-input-group-state~="disabled"] {
50
- background: var(--ui-surface-muted);
60
+ :scope:has(input:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
61
+ box-shadow:
62
+ var(--ui-control-inset),
63
+ 0 0 0 3px color-mix(in srgb, var(--ui-danger-solid) 22%, transparent);
64
+ }
65
+
66
+ /* Forced colors drop box shadows, so the focus ring becomes an outline. */
67
+ @media (forced-colors: active) {
68
+ :scope:has(input:focus-visible) {
69
+ outline: 2px solid Highlight;
70
+ outline-offset: 1px;
71
+ }
72
+ }
73
+
74
+ :scope[data-ui-input-group-state~="disabled"],
75
+ :scope:has(input:disabled) {
76
+ background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
51
77
  box-shadow: none;
78
+ cursor: not-allowed;
52
79
  }
53
80
 
54
81
  .field {
@@ -68,7 +95,17 @@
68
95
  white-space: nowrap;
69
96
  }
70
97
 
71
- .affix:empty {
98
+ /* Centred rather than padded above and below: the group is exactly a lone Input's height, so a
99
+ small button sits inside it with the difference split evenly, and the field keeps its height. */
100
+ .action {
101
+ display: flex;
102
+ flex: none;
103
+ align-items: center;
104
+ padding-inline: var(--ui-space-1);
105
+ }
106
+
107
+ .affix:empty,
108
+ .action:empty {
72
109
  display: none;
73
110
  }
74
111
 
@@ -83,7 +120,19 @@
83
120
  focus ring around the input and its affixes together; the input inside drops its own through
84
121
  its custom properties, so it is still the one ui-input, with its form behaviour intact. The
85
122
  affixes are plain text beside the input, so name the input with a label that says what the
86
- whole value is. */
123
+ whole value is. The action slot takes one button at the end, inside the border, for the one
124
+ thing the field is for ("Continue" after a site's address), so it reads as the field's action
125
+ and not the form's. */
126
+
127
+ /* The ring shows when the field's input would show its own: when it is focus-visible, which a
128
+ text input is for a pointer as well as a keyboard. A focused action button draws its own. */
129
+
130
+ /* Forced colors drop box shadows, so the focus ring becomes an outline. */
131
+ @media (forced-colors: active) {
132
+ }
133
+
134
+ /* Centred rather than padded above and below: the group is exactly a lone Input's height, so a
135
+ small button sits inside it with the difference split evenly, and the field keeps its height. */
87
136
 
88
137
  /* A component that sets its display still honours the hidden attribute on its root. */
89
138
  }
@@ -5,11 +5,11 @@
5
5
  display: inline-block;
6
6
  inline-size: 100%;
7
7
  min-inline-size: 0;
8
- min-block-size: var(--ui-control-size-md);
8
+ min-block-size: var(--ui-input-min-block-size, var(--ui-control-size-md));
9
9
  margin: 0;
10
10
  padding: var(--ui-space-2) var(--ui-space-3);
11
11
  appearance: none;
12
- border: 1px solid var(--ui-input-border, var(--ui-control-border, var(--ui-border-strong)));
12
+ border: var(--ui-input-border-width, 1px) solid var(--ui-input-border, var(--ui-control-border, var(--ui-border-strong)));
13
13
  border-radius: var(--ui-input-radius, var(--ui-radius-md));
14
14
  background: var(--ui-input-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
15
15
  color: var(--ui-text-primary);
@@ -64,6 +64,14 @@
64
64
  cursor: not-allowed;
65
65
  }
66
66
 
67
+ /* Forced colors drop box shadows, so the focus ring becomes an outline. */
68
+ @media (forced-colors: active) {
69
+ :scope:focus-visible {
70
+ outline: var(--ui-input-focus-outline, 2px solid Highlight);
71
+ outline-offset: 1px;
72
+ }
73
+ }
74
+
67
75
  /* No text-box-trim here: trimming a native input's line box below its line height lets the text
68
76
  * scroll vertically inside the field. Native inputs already center their single line. */
69
77
 
@@ -77,6 +85,10 @@
77
85
 
78
86
  /* The app's invalid flag, or the browser's own verdict once the user has left an invalid field. */
79
87
 
88
+ /* Forced colors drop box shadows, so the focus ring becomes an outline. */
89
+ @media (forced-colors: active) {
90
+ }
91
+
80
92
  /* No text-box-trim here: trimming a native input's line box below its line height lets the text
81
93
  * scroll vertically inside the field. Native inputs already center their single line. */
82
94
 
@@ -1,11 +1,13 @@
1
1
  // Generated by HTML Next 1.0.0-alpha.5 for Vanilla DOM. Do not edit.
2
- import { manageGeneratedProps } from "@nextwebwg/html-next/generated-runtime";
2
+ import { manageComponentLifecycle } from "@nextwebwg/html-next/runtime";
3
+ import * as controller from "../components/ui-input-group/ui-input-group.js";
3
4
  import "../styles/ui-input-group.css";
4
5
 
6
+ const definition = {...{"contract":{"tag":"ui-input-group","props":{"disabled":{"type":"boolean","required":false,"target":{"attribute":"disabled"},"default":false}}},"template":{"kind":"element","name":"div","attributes":[],"children":[{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"prefix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"field"}],"children":[{"kind":"slot"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"affix"}],"children":[{"kind":"slot","fallback":[],"name":"suffix"}]},{"kind":"element","name":"span","attributes":[{"kind":"literal","name":"class","value":"action"}],"children":[{"kind":"slot","fallback":[],"name":"action"}]}]},"declarations":[],"root":{"kind":"native","element":"div","choices":["div"]}},source:{file:import.meta.url},css:""};
7
+
5
8
  export function createUiInputGroup(options = {}) {
6
9
  const { attributes = {}, children = [], slots = {}, as, ...componentProps } = options;
7
10
  const projected = [];
8
- const prop0 = componentProps["disabled"] === undefined ? false : componentProps["disabled"];
9
11
  const element = document.createElement("div");
10
12
  for (const [name, value] of Object.entries(attributes)) {
11
13
  if (value === null || value === undefined || value === false) continue;
@@ -45,9 +47,20 @@ export function createUiInputGroup(options = {}) {
45
47
  } else {
46
48
  }
47
49
  element.append(element2);
48
- manageGeneratedProps(element, [
49
- { name: "disabled", attribute: "data-disabled", value: componentProps["disabled"], default: false, type: "boolean", required: false },
50
- ]);
50
+ const element3 = document.createElement("span");
51
+ element3.setAttribute("class", "action");
52
+ if (slots["action"] ?? [].length > 0) {
53
+ for (const child of slots["action"] ?? []) {
54
+ const node = typeof child === "string" ? document.createTextNode(child) : child;
55
+ projected.push([node, "action"]);
56
+ element3.append(node);
57
+ }
58
+ } else {
59
+ }
60
+ element.append(element3);
61
+ manageComponentLifecycle(element, definition, { props: componentProps, projected,
62
+ controller,
63
+ });
51
64
  return element;
52
65
  }
53
66
 
package/vue/UiInput.vue CHANGED
@@ -60,11 +60,12 @@ useComponentHost(controllerModule.default, {
60
60
  display: inline-block;
61
61
  inline-size: 100%;
62
62
  min-inline-size: 0;
63
- min-block-size: var(--ui-control-size-md);
63
+ min-block-size: var(--ui-input-min-block-size, var(--ui-control-size-md));
64
64
  margin: 0;
65
65
  padding: var(--ui-space-2) var(--ui-space-3);
66
66
  appearance: none;
67
- border: 1px solid var(--ui-input-border, var(--ui-control-border, var(--ui-border-strong)));
67
+ border: var(--ui-input-border-width, 1px) solid
68
+ var(--ui-input-border, var(--ui-control-border, var(--ui-border-strong)));
68
69
  border-radius: var(--ui-input-radius, var(--ui-radius-md));
69
70
  background: var(--ui-input-surface, var(--ui-control-surface, var(--ui-surface-elevated)));
70
71
  color: var(--ui-text-primary);
@@ -126,6 +127,14 @@ useComponentHost(controllerModule.default, {
126
127
  cursor: not-allowed;
127
128
  }
128
129
 
130
+ /* Forced colors drop box shadows, so the focus ring becomes an outline. */
131
+ @media (forced-colors: active) {
132
+ [data-component~="ui-input"]:focus-visible {
133
+ outline: var(--ui-input-focus-outline, 2px solid Highlight);
134
+ outline-offset: 1px;
135
+ }
136
+ }
137
+
129
138
  /* No text-box-trim here: trimming a native input's line box below its line height lets the text
130
139
  * scroll vertically inside the field. Native inputs already center their single line. */
131
140
 
@@ -1,13 +1,15 @@
1
1
  type __VLS_Props = {
2
2
  disabled?: boolean;
3
3
  };
4
- declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {};
4
+ declare var __VLS_1: {}, __VLS_3: {}, __VLS_5: {}, __VLS_7: {};
5
5
  type __VLS_Slots = {} & {
6
6
  prefix?: (props: typeof __VLS_1) => any;
7
7
  } & {
8
8
  default?: (props: typeof __VLS_3) => any;
9
9
  } & {
10
10
  suffix?: (props: typeof __VLS_5) => any;
11
+ } & {
12
+ action?: (props: typeof __VLS_7) => any;
11
13
  };
12
14
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
13
15
  disabled: boolean;
@@ -1,6 +1,8 @@
1
1
  <!-- Generated by HTML Next 1.0.0-alpha.5 for Vue 3.5. Do not edit. -->
2
2
  <script setup lang="ts">
3
- import { computed } from 'vue'
3
+ import { computed, ref } from 'vue'
4
+ import * as controllerModule from '../components/ui-input-group/ui-input-group.js'
5
+ import { createDispatch, useComponentHost } from './host'
4
6
 
5
7
  defineOptions({ inheritAttrs: false })
6
8
 
@@ -13,12 +15,22 @@ const props = withDefaults(
13
15
  },
14
16
  )
15
17
 
18
+ const root = ref<HTMLElement | null>(null)
19
+
16
20
  /** The values the styles' :host-state() rules test. */
17
21
  const hostState = computed(() =>
18
22
  [
19
23
  props.disabled && 'disabled',
20
24
  ].filter(Boolean).join(' ')
21
25
  )
26
+
27
+ const dispatch = createDispatch(root)
28
+
29
+ useComponentHost(controllerModule.default, {
30
+ root,
31
+ dispatch,
32
+ props,
33
+ })
22
34
  </script>
23
35
 
24
36
  <template>
@@ -26,10 +38,12 @@ const hostState = computed(() =>
26
38
  data-component="ui-input-group"
27
39
  v-bind="$attrs"
28
40
  :data-ui-input-group-state="hostState || undefined"
41
+ ref="root"
29
42
  >
30
43
  <span class="affix"><slot name="prefix" /></span>
31
44
  <span class="field"><slot /></span>
32
45
  <span class="affix"><slot name="suffix" /></span>
46
+ <span class="action"><slot name="action" /></span>
33
47
  </div>
34
48
  </template>
35
49
 
@@ -39,8 +53,13 @@ const hostState = computed(() =>
39
53
  focus ring around the input and its affixes together; the input inside drops its own through
40
54
  its custom properties, so it is still the one ui-input, with its form behaviour intact. The
41
55
  affixes are plain text beside the input, so name the input with a label that says what the
42
- whole value is. */
56
+ whole value is. The action slot takes one button at the end, inside the border, for the one
57
+ thing the field is for ("Continue" after a site's address), so it reads as the field's action
58
+ and not the form's. */
43
59
  [data-component~="ui-input-group"] {
60
+ /* The group's own border and height stand for the input's, so the group is a lone Input's height. */
61
+ --ui-input-border-width: 0;
62
+ --ui-input-min-block-size: 0;
44
63
  --ui-input-border: transparent;
45
64
  --ui-input-border-hover: transparent;
46
65
  --ui-input-focus-border: transparent;
@@ -48,6 +67,7 @@ const hostState = computed(() =>
48
67
  --ui-input-invalid-focus-shadow: none;
49
68
  --ui-input-shadow: none;
50
69
  --ui-input-focus-shadow: none;
70
+ --ui-input-focus-outline: none;
51
71
  --ui-input-radius: 0;
52
72
  --ui-input-surface: transparent;
53
73
 
@@ -57,22 +77,27 @@ const hostState = computed(() =>
57
77
  inline-size: 100%;
58
78
  min-inline-size: 0;
59
79
  min-block-size: var(--ui-control-size-md);
60
- border: 1px solid var(--ui-border-strong);
80
+ /* The frame reads the shared control tokens with a lone Input's fallbacks, so the two match. */
81
+ border: 1px solid var(--ui-control-border, var(--ui-border-strong));
61
82
  border-radius: var(--ui-radius-md);
62
- background: var(--ui-surface-elevated);
83
+ background: var(--ui-control-surface, var(--ui-surface-elevated));
63
84
  box-shadow: var(--ui-control-inset);
64
85
  overflow: hidden;
86
+ /* A press anywhere in the frame focuses the input. */
87
+ cursor: text;
65
88
  transition:
66
89
  border-color var(--ui-motion-fast) var(--ui-motion-ease),
67
90
  box-shadow var(--ui-motion-fast) var(--ui-motion-ease);
68
91
  }
69
92
 
70
- [data-component~="ui-input-group"]:hover:not(:focus-within) {
71
- border-color: var(--ui-text-secondary);
93
+ [data-component~="ui-input-group"]:hover:not(:has(input:focus, input:disabled)) {
94
+ border-color: var(--ui-control-border-hover, var(--ui-text-secondary));
72
95
  }
73
96
 
74
- [data-component~="ui-input-group"]:focus-within {
75
- border-color: var(--ui-accent-solid);
97
+ /* The ring shows when the field's input would show its own: when it is focus-visible, which a
98
+ text input is for a pointer as well as a keyboard. A focused action button draws its own. */
99
+ [data-component~="ui-input-group"]:has(.field input:focus-visible) {
100
+ border-color: var(--ui-control-focus, var(--ui-accent-solid));
76
101
  box-shadow:
77
102
  var(--ui-control-inset),
78
103
  var(--ui-control-focus-halo-shadow);
@@ -82,9 +107,25 @@ const hostState = computed(() =>
82
107
  border-color: var(--ui-danger-solid);
83
108
  }
84
109
 
85
- [data-component~="ui-input-group"][data-ui-input-group-state~="disabled"] {
86
- background: var(--ui-surface-muted);
110
+ [data-component~="ui-input-group"]:has(input:is([aria-invalid="true"], :is(:user-invalid, [data-user-invalid])):focus-visible) {
111
+ box-shadow:
112
+ var(--ui-control-inset),
113
+ 0 0 0 3px color-mix(in srgb, var(--ui-danger-solid) 22%, transparent);
114
+ }
115
+
116
+ /* Forced colors drop box shadows, so the focus ring becomes an outline. */
117
+ @media (forced-colors: active) {
118
+ [data-component~="ui-input-group"]:has(input:focus-visible) {
119
+ outline: 2px solid Highlight;
120
+ outline-offset: 1px;
121
+ }
122
+ }
123
+
124
+ [data-component~="ui-input-group"][data-ui-input-group-state~="disabled"],
125
+ [data-component~="ui-input-group"]:has(input:disabled) {
126
+ background: var(--ui-control-surface-disabled, var(--ui-surface-muted));
87
127
  box-shadow: none;
128
+ cursor: not-allowed;
88
129
  }
89
130
 
90
131
  .field {
@@ -104,7 +145,17 @@ const hostState = computed(() =>
104
145
  white-space: nowrap;
105
146
  }
106
147
 
107
- .affix:empty {
148
+ /* Centred rather than padded above and below: the group is exactly a lone Input's height, so a
149
+ small button sits inside it with the difference split evenly, and the field keeps its height. */
150
+ .action {
151
+ display: flex;
152
+ flex: none;
153
+ align-items: center;
154
+ padding-inline: var(--ui-space-1);
155
+ }
156
+
157
+ .affix:empty,
158
+ .action:empty {
108
159
  display: none;
109
160
  }
110
161
 
@@ -61,6 +61,6 @@ var UiInput_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE
61
61
  }), null, 16, _hoisted_1)), [[vModelDynamic, model.value]]);
62
62
  };
63
63
  }
64
- }), [["__scopeId", "data-v-c19f8322"]]);
64
+ }), [["__scopeId", "data-v-e7c851a2"]]);
65
65
  //#endregion
66
66
  export { UiInput_default as t };
@@ -1,10 +1,13 @@
1
1
  import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper.js";
2
- import { computed, createElementBlock, createElementVNode, defineComponent, mergeProps, openBlock, renderSlot } from "vue";
2
+ import { n as useComponentHost, t as createDispatch } from "./host.js";
3
+ import { computed, createElementBlock, createElementVNode, defineComponent, mergeProps, openBlock, ref, renderSlot } from "vue";
4
+ import * as controllerModule from "@threadlabs/looma/components/ui-input-group/ui-input-group.js";
3
5
  //#region .build/vue/UiInputGroup.vue?vue&type=script&setup=true&lang.ts
4
6
  var _hoisted_1 = ["data-ui-input-group-state"];
5
7
  var _hoisted_2 = { class: "affix" };
6
8
  var _hoisted_3 = { class: "field" };
7
9
  var _hoisted_4 = { class: "affix" };
10
+ var _hoisted_5 = { class: "action" };
8
11
  //#endregion
9
12
  //#region .build/vue/UiInputGroup.vue
10
13
  var UiInputGroup_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @__PURE__ */ defineComponent({
@@ -16,16 +19,28 @@ var UiInputGroup_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @_
16
19
  } },
17
20
  setup(__props) {
18
21
  const props = __props;
22
+ const root = ref(null);
19
23
  /** The values the styles' :host-state() rules test. */
20
24
  const hostState = computed(() => [props.disabled && "disabled"].filter(Boolean).join(" "));
25
+ const dispatch = createDispatch(root);
26
+ useComponentHost(controllerModule.default, {
27
+ root,
28
+ dispatch,
29
+ props
30
+ });
21
31
  return (_ctx, _cache) => {
22
- return openBlock(), createElementBlock("div", mergeProps({ "data-component": "ui-input-group" }, _ctx.$attrs, { "data-ui-input-group-state": hostState.value || void 0 }), [
32
+ return openBlock(), createElementBlock("div", mergeProps({ "data-component": "ui-input-group" }, _ctx.$attrs, {
33
+ "data-ui-input-group-state": hostState.value || void 0,
34
+ ref_key: "root",
35
+ ref: root
36
+ }), [
23
37
  createElementVNode("span", _hoisted_2, [renderSlot(_ctx.$slots, "prefix", {}, void 0, true)]),
24
38
  createElementVNode("span", _hoisted_3, [renderSlot(_ctx.$slots, "default", {}, void 0, true)]),
25
- createElementVNode("span", _hoisted_4, [renderSlot(_ctx.$slots, "suffix", {}, void 0, true)])
39
+ createElementVNode("span", _hoisted_4, [renderSlot(_ctx.$slots, "suffix", {}, void 0, true)]),
40
+ createElementVNode("span", _hoisted_5, [renderSlot(_ctx.$slots, "action", {}, void 0, true)])
26
41
  ], 16, _hoisted_1);
27
42
  };
28
43
  }
29
- }), [["__scopeId", "data-v-42ff173c"]]);
44
+ }), [["__scopeId", "data-v-027c11f5"]]);
30
45
  //#endregion
31
46
  export { UiInputGroup_default as t };