bitboss-ui 3.0.0-beta.45 → 3.0.0-beta.46

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 (49) hide show
  1. package/bin/bitboss-ui.mjs +6 -1
  2. package/dist/ai/BbDialog.md +12 -10
  3. package/dist/ai/BbDropzone.md +2 -2
  4. package/dist/ai/BbForm.md +2 -2
  5. package/dist/ai/BbOffCanvas.md +3 -3
  6. package/dist/ai/BbPopover.md +1 -1
  7. package/dist/ai/BbRating.md +1 -1
  8. package/dist/ai/BbTable.md +2 -2
  9. package/dist/ai/BbTabs.md +4 -4
  10. package/dist/ai/BbTextInput.md +13 -11
  11. package/dist/ai/BbTextarea.md +8 -7
  12. package/dist/ai/changelog.json +17 -3
  13. package/dist/ai/components.json +5 -4
  14. package/dist/ai/guides/installation-and-plugin-setup.md +31 -5
  15. package/dist/ai/guides/migration/v2-to-v3.md +10 -0
  16. package/dist/ai/guides/validation-libraries.md +7 -4
  17. package/dist/ai/manifest/components/BbTextInput.json +4 -3
  18. package/dist/ai/manifest/components/BbTextarea.json +2 -1
  19. package/dist/ai/manifest/index.json +2 -2
  20. package/dist/ai/manifest/meta.json +1 -0
  21. package/dist/ai/manifest/types.advanced.json +1 -0
  22. package/dist/ai/recipes/inertia/approvals-inbox.md +8 -6
  23. package/dist/ai/recipes/nuxt/approvals-inbox.md +8 -6
  24. package/dist/ai/recipes/vue/approvals-inbox.md +8 -6
  25. package/dist/ai/source/BbTextInput.md +44 -12
  26. package/dist/ai/source/BbTextarea.md +45 -13
  27. package/dist/components/BbTextInput/BbTextInput.vue.d.ts +31 -39
  28. package/dist/components/BbTextInput/BbTextInput.vue_vue_type_script_setup_true_lang.js +19 -18
  29. package/dist/components/BbTextInput/types.d.ts +17 -4
  30. package/dist/components/BbTextarea/BbTextarea.vue.d.ts +31 -37
  31. package/dist/components/BbTextarea/BbTextarea.vue_vue_type_script_setup_true_lang.js +40 -39
  32. package/dist/components/BbTextarea/types.d.ts +17 -4
  33. package/dist/composables/useSegmentedFields.js +4 -4
  34. package/dist/index.d.ts +1 -0
  35. package/dist/llms-full.txt +120 -70
  36. package/dist/llms-medium.txt +31 -5
  37. package/dist/project-dts.d.ts +8 -0
  38. package/dist/project-dts.js +6 -3
  39. package/dist/styles.css +1 -1
  40. package/dist/types/Config.d.ts +26 -8
  41. package/dist/utils/versionCheck.js +1 -1
  42. package/dist/vite-plugin.d.ts +6 -4
  43. package/dist/vite.js +182 -181
  44. package/package.json +1 -1
  45. package/scripts/lib/eslint-disable.mjs +2 -0
  46. package/scripts/lib/eslint-plugin.d.ts +2 -0
  47. package/scripts/lib/eslint-plugin.mjs +117 -0
  48. package/scripts/lib/text-null-value.mjs +260 -0
  49. package/scripts/lib/validate-bb-markup.mjs +4 -0
@@ -76,6 +76,7 @@ import {
76
76
  projectLocalBbComponents,
77
77
  } from '../scripts/lib/local-components.mjs';
78
78
  import { applyBitbossFixes } from '../scripts/lib/check-fix.mjs';
79
+ import { stringControlsFor } from '../scripts/lib/text-null-value.mjs';
79
80
  import { cssClassFindings } from '../scripts/lib/css-class-check.mjs';
80
81
  import {
81
82
  cssLocalFindings,
@@ -1044,7 +1045,11 @@ async function checkCommand(
1044
1045
  const content = readText(file);
1045
1046
  const { findings: fileFindings } = file.endsWith('.md')
1046
1047
  ? validateMarkdown(content, manifest, validateOptions)
1047
- : validateVueSnippet(content, manifest, validateOptions);
1048
+ : validateVueSnippet(content, manifest, {
1049
+ ...validateOptions,
1050
+ // The app's null-value policy, from the project bitboss-ui.d.ts.
1051
+ stringControls: stringControlsFor(file),
1052
+ });
1048
1053
  // Q46.9: a finding the author silenced for ESLint (`eslint-disable*`,
1049
1054
  // usually with a reason) is silenced here too, or `check` cannot gate
1050
1055
  // CI next to a lint step that passes. `.md` lines are fence-relative,
@@ -99,7 +99,7 @@ back to `false`, which is the same thing.
99
99
  <template #footer>
100
100
  <div class="flex justify-end gap-2">
101
101
  <BbButton variant="ghost" @click="open = false">Cancel</BbButton>
102
- <BbButton :disabled="!name.trim()" variant="primary" @click="create">
102
+ <BbButton :disabled="!name?.trim()" variant="primary" @click="create">
103
103
  Create
104
104
  </BbButton>
105
105
  </div>
@@ -112,7 +112,7 @@ import { ref } from 'vue';
112
112
  import { BbButton, BbDialog, BbTextInput } from 'bitboss-ui';
113
113
 
114
114
  const open = ref(false);
115
- const name = ref('');
115
+ const name = ref<string | null>('');
116
116
 
117
117
  // Re-seed on `@show`, not on close — a dismissed dialog may be reopened.
118
118
  const reset = () => {
@@ -442,7 +442,7 @@ const roles = [
442
442
  ];
443
443
 
444
444
  const open = ref(false);
445
- const email = ref('');
445
+ const email = ref<string | null>('');
446
446
  const role = ref('member');
447
447
 
448
448
  // Re-seed on `@show`, not on close — a dismissed dialog may be reopened.
@@ -452,7 +452,9 @@ const reset = () => {
452
452
  };
453
453
 
454
454
  // Primary action stays disabled until the email is plausible.
455
- const canSend = computed(() => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value));
455
+ const canSend = computed(() =>
456
+ /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value ?? '')
457
+ );
456
458
 
457
459
  const send = () => {
458
460
  // Send the invite here, then close only on success.
@@ -600,7 +602,7 @@ element.
600
602
  <div class="flex justify-end gap-2">
601
603
  <!-- Keep a visible way out — never trap the user. -->
602
604
  <BbButton variant="ghost" @click="open = false">Skip</BbButton>
603
- <BbButton :disabled="!company.trim()" variant="primary" @click="save">
605
+ <BbButton :disabled="!company?.trim()" variant="primary" @click="save">
604
606
  Save
605
607
  </BbButton>
606
608
  </div>
@@ -613,8 +615,8 @@ import { ref } from 'vue';
613
615
  import { BbButton, BbDialog, BbTextInput } from 'bitboss-ui';
614
616
 
615
617
  const open = ref(false);
616
- const displayName = ref('');
617
- const company = ref('');
618
+ const displayName = ref<string | null>('');
619
+ const company = ref<string | null>('');
618
620
 
619
621
  // Re-seed on `@show` so a dismissed step reopens clean.
620
622
  const reset = () => {
@@ -676,7 +678,7 @@ automatically; close only once the work resolves:
676
678
  Cancel
677
679
  </BbButton>
678
680
  <!-- Returning the promise lets BbButton run its own loading state. -->
679
- <BbButton :disabled="!draft.trim()" variant="primary" @click="save">
681
+ <BbButton :disabled="!draft?.trim()" variant="primary" @click="save">
680
682
  Rename
681
683
  </BbButton>
682
684
  </div>
@@ -692,7 +694,7 @@ import { BbButton, BbDialog, BbTextInput } from 'bitboss-ui';
692
694
  const projectName = ref('acme-storefront');
693
695
 
694
696
  const open = ref(false);
695
- const draft = ref('');
697
+ const draft = ref<string | null>('');
696
698
  const saving = ref(false);
697
699
 
698
700
  // Re-seed on `@show` so a dismissed rename never resurfaces half-typed.
@@ -706,7 +708,7 @@ const save = async () => {
706
708
  try {
707
709
  // Simulated request — replace with your API call.
708
710
  await new Promise((resolve) => setTimeout(resolve, 600));
709
- projectName.value = draft.value.trim();
711
+ projectName.value = (draft.value ?? '').trim();
710
712
  // Work first, then close: the transition runs while your UI refreshes.
711
713
  open.value = false;
712
714
  } finally {
@@ -1269,7 +1269,7 @@ import { ref } from 'vue';
1269
1269
  import { BbDropzone, BbButton, BbIcon, BbTextInput } from 'bitboss-ui';
1270
1270
  import type { DropZoneError } from 'bitboss-ui';
1271
1271
 
1272
- const title = ref('');
1272
+ const title = ref<string | null>('');
1273
1273
  const documents = ref<File[]>([]);
1274
1274
  const errors = ref<string[]>([]);
1275
1275
  const submitted = ref<{ field: string; fileCount: number } | null>(null);
@@ -1298,7 +1298,7 @@ const remove = (file: File) => {
1298
1298
  model — assemble a FormData and hand it to your request layer. */
1299
1299
  const submit = () => {
1300
1300
  const body = new FormData();
1301
- body.append('title', title.value);
1301
+ body.append('title', title.value ?? '');
1302
1302
  documents.value.forEach((file) => body.append('documents[]', file));
1303
1303
  submitted.value = { field: 'documents[]', fileCount: documents.value.length };
1304
1304
  };
package/dist/ai/BbForm.md CHANGED
@@ -310,8 +310,8 @@ Four things to know:
310
310
  - **An emptied text field holds `null` by default**, not `''`. So
311
311
  `z.string().min(1, 'Required')` alone answers an emptied field with the
312
312
  library's own "expected string, received null". Two ways out:
313
- - **Set the plugin option `formControls: { textInputNullValue: '',
314
- textareaNullValue: '' }`.** Emptied fields then emit `''` and the short
313
+ - **Set the plugin option `formControls: { textInputNullValue: 'string',
314
+ textareaNullValue: 'string' }`.** Emptied fields then emit `''` and the short
315
315
  schema shows your message as written. Start text values as `''` too (a
316
316
  field nobody touched keeps what your page gave it).
317
317
  - **Or keep `null` and put the message on the type as well:**
@@ -216,7 +216,7 @@ header and footer stay pinned.
216
216
  <div class="flex justify-end gap-2">
217
217
  <BbButton variant="ghost" @click="open = false">Cancel</BbButton>
218
218
  <BbButton
219
- :disabled="!email.trim()"
219
+ :disabled="!email?.trim()"
220
220
  variant="primary"
221
221
  @click="open = false"
222
222
  >
@@ -232,7 +232,7 @@ import { ref } from 'vue';
232
232
  import { BbButton, BbOffCanvas, BbTextInput } from 'bitboss-ui';
233
233
 
234
234
  const open = ref(false);
235
- const email = ref('');
235
+ const email = ref<string | null>('');
236
236
  </script>
237
237
  ```
238
238
 
@@ -951,7 +951,7 @@ import { ref } from 'vue';
951
951
  import { BbButton, BbOffCanvas, BbTextInput } from 'bitboss-ui';
952
952
 
953
953
  const open = ref(false);
954
- const reviewer = ref('');
954
+ const reviewer = ref<string | null>('');
955
955
  </script>
956
956
  ```
957
957
 
@@ -332,7 +332,7 @@ dismissible.
332
332
  import { ref } from 'vue';
333
333
  import { BbButton, BbPopover, BbTextInput } from 'bitboss-ui';
334
334
 
335
- const days = ref('90');
335
+ const days = ref<string | null>('90');
336
336
  </script>
337
337
  ```
338
338
 
@@ -273,7 +273,7 @@ import { ref } from 'vue';
273
273
  import { BbButton, BbRating, BbTextarea } from 'bitboss-ui';
274
274
 
275
275
  const score = ref<number | null>(null);
276
- const feedback = ref('');
276
+ const feedback = ref<string | null>('');
277
277
  const errors = ref<string | undefined>();
278
278
  const sent = ref(false);
279
279
 
@@ -849,13 +849,13 @@ const catalogue: Invoice[] = [
849
849
  { id: '4', number: 'INV-1044', client: 'Umbrella', amount: '€2,750' },
850
850
  ];
851
851
 
852
- const search = ref('');
852
+ const search = ref<string | null>('');
853
853
 
854
854
  // Real apps forward the query to the backend; here we filter a local catalogue
855
855
  // after a short simulated round-trip.
856
856
  const fetchInvoices = async (): Promise<Invoice[]> => {
857
857
  await new Promise((resolve) => setTimeout(resolve, 250));
858
- const term = search.value.trim().toLowerCase();
858
+ const term = (search.value ?? '').trim().toLowerCase();
859
859
  if (!term) return catalogue;
860
860
  return catalogue.filter(
861
861
  (invoice) =>
package/dist/ai/BbTabs.md CHANGED
@@ -418,7 +418,7 @@ nothing when you flip back:
418
418
  </template>
419
419
  <template #preview>
420
420
  <div class="grid gap-1.5 pt-3 text-sm whitespace-pre-line">
421
- <template v-if="draft.trim()">
421
+ <template v-if="draft?.trim()">
422
422
  <p class="font-medium">{{ previewTitle }}</p>
423
423
  <p class="text-(--bb-text-muted)">{{ previewBody }}</p>
424
424
  </template>
@@ -442,17 +442,17 @@ const tabs: Array<BbTabsItem<Mode>> = [
442
442
  { key: 'preview', label: 'Preview' },
443
443
  ];
444
444
 
445
- const draft = ref(
445
+ const draft = ref<string | null>(
446
446
  '## Billing retries\n\nVerified on staging — invoice totals now match the ledger.'
447
447
  );
448
448
 
449
449
  const previewTitle = computed(() => {
450
- const heading = draft.value.match(/^##\s+(.+)$/m);
450
+ const heading = (draft.value ?? '').match(/^##\s+(.+)$/m);
451
451
  return heading?.[1] ?? 'Untitled';
452
452
  });
453
453
 
454
454
  const previewBody = computed(() =>
455
- draft.value.replace(/^##\s+.+$/m, '').trim()
455
+ (draft.value ?? '').replace(/^##\s+.+$/m, '').trim()
456
456
  );
457
457
  </script>
458
458
  ```
@@ -62,7 +62,7 @@ Pick a sibling when the shape of the data is different:
62
62
  - calendar dates → `BbDatePickerInput`;
63
63
  - choosing from a known set of options → `BbSelect` / `BbRadioGroup`.
64
64
 
65
- `v-model` binds a `string | null` — an emptied field emits `null` by default, or `''` when the plugin option `formControls` says so (see the installation guide § `formControls`).
65
+ `v-model` binds a `string | null` — an emptied field emits `null` by default, or `''` when the plugin option `formControls` sets the `'string'` policy (see the installation guide § `formControls`). One field can differ from the app with `null-value="string"` or `null-value="null"`; the `update:modelValue` payload types to match (`string` with `'string'`).
66
66
  `label` is required (hide it visually with `hideLabel`, never drop it).
67
67
 
68
68
  ### Label & label modes
@@ -233,7 +233,7 @@ name. The slot alone is enough: no `description` prop needed.
233
233
  import { ref } from 'vue';
234
234
  import { BbTextInput } from 'bitboss-ui';
235
235
 
236
- const subdomain = ref('');
236
+ const subdomain = ref<string | null>('');
237
237
  </script>
238
238
  ```
239
239
 
@@ -403,8 +403,9 @@ const apiKey = 'sk_live_51Hn3x8Kj2';
403
403
  ### Clearable & loading
404
404
 
405
405
  `clearable` reveals a clear button while the field has a value and is hovered
406
- or focused; clicking it emits `update:modelValue` with `null` and refocuses the
407
- input. There is no separate `clear` event — watch the model for `null`. The
406
+ or focused; clicking it emits `update:modelValue` with the empty value (`null`
407
+ by default) and refocuses the input. There is no separate `clear` event — watch
408
+ the model for the empty value. The
408
409
  button is suppressed on `disabled`/`readonly` fields.
409
410
 
410
411
  `loading` shows a spinner in the append position. It is **visual only** — the
@@ -416,7 +417,7 @@ The classic pairing is a signup handle checker: debounce keystrokes, flip
416
417
  `loading` while the (simulated) availability call runs, then land the result
417
418
  in `errors` or `hint`. Because the field stays editable during the check, the
418
419
  handler tags each request and drops stale responses. Watching the model —
419
- rather than listening to `input` — means the clear button's `null` also resets
420
+ rather than listening to `input` — means the clear button's empty value also resets
420
421
  the status.
421
422
 
422
423
  **Async username check**
@@ -501,7 +502,7 @@ Pass a [maska](https://beholdr.github.io/maska/v3) config to `mask` to format
501
502
  as the user types — card numbers, phone numbers, license keys. By default
502
503
  `v-model` receives the _unmasked_ value (`'4242424242424242'` for the card
503
504
  below); set `emitMasked` to persist the formatted string instead. A fully
504
- emptied masked field emits the empty value (`null` by default, `''` with `formControls`), like every other state of this component.
505
+ emptied masked field emits the empty value (`null` by default, `''` with the `'string'` policy), like every other state of this component.
505
506
 
506
507
  The mask is live-reactive: changing the `mask` options at runtime reformats the
507
508
  current value, and toggling `emitMasked` re-emits the model in the new format.
@@ -611,7 +612,7 @@ const revealed = ref(false);
611
612
 
612
613
  ### Events & v-model
613
614
 
614
- - `update:modelValue` — every value change; `string`, or the empty value when emptied (`null` by default, `''` with `formControls`)
615
+ - `update:modelValue` — every value change; `string`, or the empty value when emptied (the `null-value` policy, else `formControls`, else `'null'`; `'string'` emits `''`)
615
616
  (with a mask: the unmasked/masked string per `emitMasked`).
616
617
  - `focus` / `blur` — forwarded `FocusEvent`s; they also drive the hint's
617
618
  show-on-focus behavior.
@@ -1194,7 +1195,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1194
1195
 
1195
1196
  ### Gotchas & anti-patterns
1196
1197
 
1197
- - An emptied field emits `null` by default (`''` with the `formControls` plugin option) — write `value ?? ''` when feeding
1198
+ - An emptied field emits `null` by default (`''` with `null-value="string"` or the `formControls` plugin option) — write `value ?? ''` when feeding
1198
1199
  APIs that require strings.
1199
1200
  - `layout` is ignored under `labelMode="floating"`/`"inside"` — those modes
1200
1201
  embed the label in the field and force the vertical layout (see _Density &
@@ -1256,7 +1257,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1256
1257
  | `hint` | `string \| undefined` | | | Text box to be displayed near the input, usually to indicate instructions. |
1257
1258
  | `id` | `string \| undefined` | | | The `id` of the native control; the label's `for` and the description and message ids derive from it. Generated when omitted. |
1258
1259
  | `inputAlign` | `'top'` \| `'center'` \| `'bottom'` | | | Vertical position of the control in a side-by-side row; not `inputPosition`, the horizontal one. Omitted, the control's first line meets the label's. |
1259
- | `inputMode` | `"text" \| "none" \| "search" \| "email" \| "url" \| "tel" \| "numeric" \| "decimal" \| undefined` | | | The inputmode of the input. |
1260
+ | `inputMode` | `"text" \| "none" \| "search" \| "tel" \| "url" \| "email" \| "numeric" \| "decimal" \| undefined` | | | The inputmode of the input. |
1260
1261
  | `label` | `string` | | yes | Text content of the label of the element. |
1261
1262
  | `labelAlign` | `'top'` \| `'center'` \| `'bottom'` | | | Vertical position of the label block in a side-by-side row; not `labelPosition`, the horizontal one. Omitted, the label's first line meets the control's. |
1262
1263
  | `labelMode` | `"outside" \| "floating" \| "inside" \| undefined` | | | Label rendering mode. |
@@ -1266,6 +1267,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1266
1267
  | `mask` | `MaskInputOptions \| undefined` | | | The mask to be applied to the input. Please visit https://beholdr.github.io/maska/v3 for syntax examples. |
1267
1268
  | `modelValue` | `string \| null \| undefined` | | | Used by v-model. |
1268
1269
  | `name` | `string \| undefined` | | | Defines the name of the input. |
1270
+ | `nullValue` | `E \| undefined` | | | What the field emits once the user empties it (typing it empty, the clear button, a mask with nothing in it): `'null'` emits `null`, `'string'` emits `''`. Unset, the app's `formControls.textInputNullValue` applies (`'null'` by default). Also types the `update:modelValue` payload: with `'string'` it is `string`. |
1269
1271
  | `persistentHint` | `boolean \| undefined` | `false` | | Keeps the hint displayed. |
1270
1272
  | `placeholder` | `string \| undefined` | | | String displayed when there's no data. |
1271
1273
  | `prepend:icon` | `string \| undefined` | | | Name of the icon to be added at the start of the input. |
@@ -1273,7 +1275,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1273
1275
  | `required` | `boolean \| undefined` | `false` | | Sets the input as required. |
1274
1276
  | `reverse` | `boolean \| undefined` | `false` | | Reverses the layout. Applicable in every direction the order of the label and the input is swapped. |
1275
1277
  | `rules` | `RuleExpression<string \| null>` | | | vee-validate rules for this field. Only inside a `BbForm`. |
1276
- | `type` | `"text" \| "search" \| "email" \| "url" \| "tel" \| "password" \| undefined` | `"text"` | | Type of the input. Restricted to the textual input types this component supports — use `BbNumberInput` for numbers and `BbDatePickerInput` for dates. |
1278
+ | `type` | `"text" \| "search" \| "tel" \| "url" \| "email" \| "password" \| undefined` | `"text"` | | Type of the input. Restricted to the textual input types this component supports — use `BbNumberInput` for numbers and `BbDatePickerInput` for dates. |
1277
1279
  | `validationLabel` | `string \| undefined` | | | The field's name in validation messages (`{field}`): "Email is not valid". Defaults to the `label` (or `legend`), then "This field"; the field's `name` never shows. Set it when the label is not a good noun for a sentence (an icon-only label, a question) or is hidden. Only inside a `BbForm`. |
1278
1280
  | `validationMode` | `'aggressive'` \| `'balanced'` \| `'lazy'` \| `'submit-only'` \| `'submit-live'` | | | When this field checks itself while the user works: `aggressive` (every change), `balanced` (on leave, then every change while it shows an error), `lazy` (on leave only), `submit-only` (on submit only) or `submit-live` (on submit, then every change while it shows an error). "Leave" is blur for a text-like control, and focus leaving the whole control for a picker or a group; a checkbox or switch commits on click. Unset, the enclosing `BbForm`'s `validation-mode` applies, then the app's `formControls.validationMode` (`'balanced'`). Only inside a `BbForm`. |
1279
1281
  | `variant` | `'outline'` \| `'secondary'` \| `'none'` \| `'ghost'` | `'outline'` | | Visual variant of the field box — the same names and tokens as the `BbButton` variants. Colours only: height, padding and border width are identical across variants, so a form never reflows when one changes. `'ghost'` has no border in any state (errors and warnings show through the icon and the messages); every variant keeps the focus ring. Register extra names with the plugin's `inputVariants` option. `'none'` is a blank starting point: the field keeps its structure and behaviour but takes no look, so your own classes don't fight a built-in one. You then own hover, focus, disabled and the error / warning states. Fine for a one-off; keep inline styling short, and register a real variant (`inputVariants`) when the look recurs. |
@@ -1326,7 +1328,7 @@ States are listed in precedence order: when two are on at once and their entries
1326
1328
  - `mousedown` — `(event: MouseEvent): void` — Emitted when a pointing device button is pressed over the input. Forwards the original DOM `MouseEvent`.
1327
1329
  - `mouseup` — `(event: MouseEvent): void` — Emitted when a pointing device button is released over the input. Forwards the original DOM `MouseEvent`.
1328
1330
  - `paste` — `(event: ClipboardEvent): void` — Emitted when content is pasted into the input. Forwards the original DOM `ClipboardEvent`.
1329
- - `update:modelValue` — `(value: string \| null): void` — Emitted when the value changes. Carries the new string or `null` when cleared. When `mask` is set, emits the masked or unmasked string depending on `emitMasked`.
1331
+ - `update:modelValue` — `(value: string \| NullValueOf<E>): void` — Emitted when the value changes. Carries the new string, or the empty value (`null-value`, else the app's, `null` by default) when emptied. When `mask` is set, emits the masked or unmasked string depending on `emitMasked`.
1330
1332
 
1331
1333
  ## Slots
1332
1334
 
@@ -63,7 +63,7 @@ Pick a sibling when the shape of the data is different:
63
63
  - a calendar date → `BbDatePickerInput`;
64
64
  - a value from a known set → `BbSelect` / `BbRadioGroup`.
65
65
 
66
- `v-model` binds a `string | null` — an emptied field emits `null` by default, or `''` when the plugin option `formControls` says so (see the installation guide § `formControls`).
66
+ `v-model` binds a `string | null` — an emptied field emits `null` by default, or `''` when the plugin option `formControls` sets the `'string'` policy (see the installation guide § `formControls`). One field can differ from the app with `null-value="string"` or `null-value="null"`; the `update:modelValue` payload types to match (`string` with `'string'`).
67
67
  `label` is required (hide it visually with `hideLabel`, never drop it).
68
68
 
69
69
  ### Label
@@ -608,9 +608,9 @@ const snippet = 'npx bitboss-ui ai-init';
608
608
  ### Clearable & loading
609
609
 
610
610
  `clearable` reveals a clear button while the field has a value and is hovered or
611
- focused; clicking it emits `update:modelValue` with `null` and is suppressed on
612
- `disabled`/`readonly` fields. There is no separate `clear` event — watch the
613
- model for `null`.
611
+ focused; clicking it emits `update:modelValue` with the empty value (`null` by
612
+ default) and is suppressed on `disabled`/`readonly` fields. There is no separate
613
+ `clear` event — watch the model for the empty value.
614
614
 
615
615
  `loading` shows a spinner in the append position. It is **visual only** — the
616
616
  field stays editable, so pair it with your own guard if the value must not
@@ -803,7 +803,7 @@ const notes = ref<string | null>(null);
803
803
 
804
804
  ### Events & v-model
805
805
 
806
- - `update:modelValue` — every value change; `string`, or the empty value when emptied (`null` by default, `''` with `formControls`).
806
+ - `update:modelValue` — every value change; `string`, or the empty value when emptied (the `null-value` policy, else `formControls`, else `'null'`; `'string'` emits `''`).
807
807
  - `focus` / `blur` — forwarded `FocusEvent`s; they also drive the hint's
808
808
  show-on-focus behavior.
809
809
  - `input` — every keystroke (raw DOM event); `change` — on commit (blur), the
@@ -979,7 +979,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
979
979
  floor; the field starts there and never shrinks below it.
980
980
  - Avoid `labelMode="floating"` with `placeholder` — the resting label and the
981
981
  placeholder occupy the same spot.
982
- - An emptied field emits `null` by default (`''` with the `formControls` plugin option) — write `value ?? ''` when feeding
982
+ - An emptied field emits `null` by default (`''` with `null-value="string"` or the `formControls` plugin option) — write `value ?? ''` when feeding
983
983
  APIs that require strings.
984
984
  - `counter` counts what `maxlength` counts: UTF-16 code units, so an emoji
985
985
  counts 2, and a line break counts 1 in the browser but 2 once a form
@@ -1044,6 +1044,7 @@ A `pt` value written in `<script>` (a constant, a preset, a wrapper's
1044
1044
  | `loading` | `boolean \| undefined` | `false` | | Sets the component in a loading state, usually triggering some visual styles. |
1045
1045
  | `modelValue` | `string \| null \| undefined` | | | Used by v-model. |
1046
1046
  | `name` | `string \| undefined` | | | Defines the name of the input. |
1047
+ | `nullValue` | `E \| undefined` | | | What the field emits once the user empties it (typing it empty, the clear button): `'null'` emits `null`, `'string'` emits `''`. Unset, the app's `formControls.textareaNullValue` applies (`'null'` by default). Also types the `update:modelValue` payload: with `'string'` it is `string`. |
1047
1048
  | `persistentHint` | `boolean \| undefined` | `false` | | Keeps the hint displayed. |
1048
1049
  | `placeholder` | `string \| undefined` | | | String displayed when there's no data. |
1049
1050
  | `prepend:icon` | `string \| undefined` | | | Name of the icon to be added at the start of the input. |
@@ -1104,7 +1105,7 @@ States are listed in precedence order: when two are on at once and their entries
1104
1105
  - `mousedown` — `(event: MouseEvent): void` — Emitted when a pointing device button is pressed over the textarea. Forwards the original DOM `MouseEvent`.
1105
1106
  - `mouseup` — `(event: MouseEvent): void` — Emitted when a pointing device button is released over the textarea. Forwards the original DOM `MouseEvent`.
1106
1107
  - `paste` — `(event: ClipboardEvent): void` — Native clipboard paste. Forwards the original DOM `ClipboardEvent`.
1107
- - `update:modelValue` — `(value: string \| null): void` — Emitted when the value changes. Carries the new text or `null` when cleared.
1108
+ - `update:modelValue` — `(value: string \| NullValueOf<E>): void` — Emitted when the value changes. Carries the new text, or the empty value (`null-value`, else the app's, `null` by default) when emptied.
1108
1109
 
1109
1110
  ## Slots
1110
1111
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 3,
3
3
  "library": "bitboss-ui",
4
- "version": "3.0.0-beta.45",
4
+ "version": "3.0.0-beta.46",
5
5
  "upgrade": "v2-to-v3",
6
6
  "guide": "ai/guides/migration/v2-to-v3.md",
7
7
  "legend": {
@@ -56,7 +56,8 @@
56
56
  "3.0.0-beta.42",
57
57
  "3.0.0-beta.43",
58
58
  "3.0.0-beta.44",
59
- "3.0.0-beta.45"
59
+ "3.0.0-beta.45",
60
+ "3.0.0-beta.46"
60
61
  ],
61
62
  "unpublished": [
62
63
  "3.0.0-alpha.2",
@@ -77,7 +78,7 @@
77
78
  ],
78
79
  "summary": {
79
80
  "renames": 213,
80
- "behaviourBreaks": 168,
81
+ "behaviourBreaks": 169,
81
82
  "bySurface": {
82
83
  "prop": 15,
83
84
  "option": 1,
@@ -5541,6 +5542,19 @@
5541
5542
  "ruling": "owner 2026-10-01 (popover-in-dialog close)",
5542
5543
  "guide": "v2-to-v3.md § 6 item 41 + BbDialog.guide.md / BbOffCanvas.guide.md § Lifecycle events",
5543
5544
  "firstReleasedIn": "3.0.0-beta.44"
5545
+ },
5546
+ {
5547
+ "id": "f302",
5548
+ "kind": "behaviour",
5549
+ "summary": "`formControls.textInputNullValue` / `textareaNullValue` values `null` / `''` → `'null'` / `'string'`; an old `''` falls back to `null`",
5550
+ "components": [
5551
+ "BbTextInput",
5552
+ "BbTextarea"
5553
+ ],
5554
+ "description": "formControls.textInputNullValue / textareaNullValue take a policy instead of the value: 'null' (the default) emits null, 'string' emits '' (it was null / ''). An old '' now falls back to null silently; TypeScript flags it in vite.config / app.use. The new per-field null-value prop takes the same values, and the Vite option also types update:modelValue (string with 'string') through the project bitboss-ui.d.ts",
5555
+ "ruling": "owner 2026-10-05 (null-value policy)",
5556
+ "guide": "v2-to-v3.md § 6 item 42 + installation-and-plugin-setup.md § formControls",
5557
+ "firstReleasedIn": "3.0.0-beta.46"
5544
5558
  }
5545
5559
  ]
5546
5560
  }