@omnitend/dashboard-for-laravel 0.46.0 → 0.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -215,6 +215,19 @@ export interface FieldDefinition {
215
215
  * `FormFieldDefinition` re-declares this as required for inference.)
216
216
  */
217
217
  default?: any;
218
+ /**
219
+ * For a field whose value is a list of rows (a `repeater`, or a span
220
+ * field that renders its own rows and owns `key.N.*` error keys): the
221
+ * row's own name, called with the row (`model[key][index]`, or `{}` when
222
+ * the model has no such row) and its 0-based index. When it returns a
223
+ * non-empty string, the error summary lists that row's errors under it
224
+ * alone ("Chaucer Ale (Pint): Enter a price.") instead of
225
+ * "<sub-field label> (line N)". Returning `undefined`, `null` or `""`
226
+ * (a row with no name yet), or throwing, keeps the line-number label.
227
+ * Only labels: which field owns the error, and what a click on the
228
+ * summary row focuses, are unchanged.
229
+ */
230
+ rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
218
231
  /** Label for a repeater's "add row" button (default: "Add"). */
219
232
  addLabel?: string;
220
233
  /** Minimum / maximum number of repeater rows. */
@@ -295,6 +308,10 @@ export interface FieldDefinition {
295
308
  * every `editFields` key, so those decorative fields are POSTed alongside
296
309
  * the real ones (an empty string, an empty array) purely because they were
297
310
  * declared.
311
+ *
312
+ * Applies per KEY (#228): DXTable drops a key only when no field with that
313
+ * key is both submitting and visible, so a `submit: false` display twin
314
+ * does not strip the value of a visible editable field sharing its key.
298
315
  */
299
316
  submit?: boolean;
300
317
  /**
@@ -444,6 +461,7 @@ export interface ComponentFieldDef extends BaseFieldDef {
444
461
  export interface RepeaterFieldDef extends BaseFieldDef {
445
462
  type: "repeater";
446
463
  fields?: FieldDef[];
464
+ rowLabel?: (row: Record<string, any>, index: number) => string | null | undefined;
447
465
  addLabel?: string;
448
466
  minItems?: number;
449
467
  maxItems?: number;
@@ -488,6 +506,48 @@ export interface FormTab {
488
506
  /** Lazily mount tab content until first activated */
489
507
  lazy?: boolean;
490
508
  }
509
+ /**
510
+ * Slot props of DXField's `password-actions` slot (DXForm:
511
+ * `password-actions(<key>)`): content placed beside a password input, inside
512
+ * its input group after the reveal (eye) toggle, such as a "Generate password"
513
+ * button. `setRevealed` drives the same state the eye toggles.
514
+ *
515
+ * ```vue
516
+ * <template #password-actions(password)="{ update, setRevealed, disabled }">
517
+ * <DButton variant="outline-secondary" :disabled="disabled"
518
+ * @click="update(generatePassword()); setRevealed(true)">Generate</DButton>
519
+ * </template>
520
+ * ```
521
+ */
522
+ export interface PasswordActionsSlotProps {
523
+ /** The field definition being rendered. */
524
+ field: FieldDefinition;
525
+ /** The model passed to field predicates (defaults to the live form data). */
526
+ model: any;
527
+ /** The current field value. */
528
+ value: any;
529
+ /** Write a new value (also marks the field touched and clears its error). */
530
+ update: (value: any) => void;
531
+ /** Whether the password is currently shown as plain text. */
532
+ revealed: boolean;
533
+ /**
534
+ * Show (`true`) or mask (`false`) the password, exactly as the eye does.
535
+ * A no-op on a `revealable: false` field, which never unmasks.
536
+ */
537
+ setRevealed: (revealed: boolean) => void;
538
+ /** Whether the field is disabled or read-only. */
539
+ disabled: boolean;
540
+ }
541
+ /**
542
+ * Slot props of DXTable's `edit-password-actions(<key>)` slot, the edit/create
543
+ * modal's form of {@link PasswordActionsSlotProps}. `item` is the row being
544
+ * edited, `null` in create mode. (`model` is not passed, as with the modal's
545
+ * other field slots.)
546
+ */
547
+ export interface EditPasswordActionsSlotProps extends Omit<PasswordActionsSlotProps, "model"> {
548
+ /** The row being edited; `null` in create mode. */
549
+ item: any;
550
+ }
491
551
  /**
492
552
  * The theme's extra colour variant, `pending`: waiting, and the next move is
493
553
  * not yours ("awaiting payment", "sent to printer, not yet confirmed").
@@ -25,7 +25,10 @@ export interface ErrorTarget {
25
25
  fieldKey: string | null;
26
26
  /** The tab holding the owning field, or `null` (flat form, or unowned). */
27
27
  tabKey: string | null;
28
- /** A readable name for the key: "Price (line 1)", "Delivery date". */
28
+ /**
29
+ * A readable name for the key: "Price (line 1)", "Delivery date", or a
30
+ * row's own name from the field's `rowLabel`.
31
+ */
29
32
  label: string;
30
33
  }
31
34
  export interface ResolveErrorTargetsOptions {
@@ -75,7 +78,8 @@ export declare function humaniseErrorKey(key: string): string;
75
78
  *
76
79
  * Labels: the owning field's label; for `field.N.sub` the sub-field's label
77
80
  * (when the field defines `fields` and one has that key) or the field's own,
78
- * plus "(line N+1)"; for any other nested key (a media map's
81
+ * plus "(line N+1)" — unless the field's `rowLabel` names that row, when
82
+ * the row's name is the whole label; for any other nested key (a media map's
79
83
  * `image_media.<uuid>`) the field's label. An unowned key is humanised.
80
84
  *
81
85
  * Display lines: "Label: message", except that the label is left off when
@@ -57,3 +57,19 @@ export declare function isFieldVisible(field: {
57
57
  export declare function isSubmittableField(field: {
58
58
  submit?: boolean;
59
59
  }): boolean;
60
+ /**
61
+ * The payload keys to drop on submit: every field key for which NO field with
62
+ * that key is both submittable (`submit !== false`) and visible (#228).
63
+ *
64
+ * Decided per KEY, across all the fields that share it, never per field. Two
65
+ * fields may share a key: an editable select plus a read-only display twin
66
+ * (`submit: false`) that shows instead for system-created rows. Stripping a
67
+ * key because ONE of its fields is presentational or hidden removed it from
68
+ * every save, including while the select was the visible field in use. A key
69
+ * whose fields are all presentational (#110) or all hidden (#117) is still
70
+ * dropped.
71
+ */
72
+ export declare function nonSubmittedFieldKeys<TField extends {
73
+ key: string;
74
+ submit?: boolean;
75
+ }>(fields: readonly TField[], isVisible: (field: TField) => boolean): Set<string>;
@@ -1,8 +1,8 @@
1
1
  {
2
- "generated": "2026-10-15T11:16:01.160Z",
2
+ "generated": "2026-10-21T10:08:54.451Z",
3
3
  "package": {
4
4
  "name": "@omnitend/dashboard-for-laravel",
5
- "version": "0.46.0"
5
+ "version": "0.47.0"
6
6
  },
7
7
  "components": {
8
8
  "base": [
@@ -2118,7 +2118,7 @@
2118
2118
  "name": "DXField",
2119
2119
  "category": "extended",
2120
2120
  "filePath": "resources/js/components/extended/DXField.vue",
2121
- "description": "Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `info-popover`, `hint` and `repeater-row` slots for per-field customisation. Structure (#135): a thin DISPATCHER. The shared chrome (label + validation feedback + hint + help + info) lives in `DXFieldShell`; the numeric controls (currency/percentage/number) in `DXNumericField`; the choice controls (select/searchable/radio/checkbox-group) in `DXChoiceField`; and the async option loading in the `useAsyncOptions` composable. This file decides which control to render inside the shell by `field.type`. None of those pieces are exported — they exist only to keep this component factored.",
2121
+ "description": "Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `info-popover`, `hint`, `repeater-row` and `password-actions` slots for per-field customisation. Structure (#135): a thin DISPATCHER. The shared chrome (label + validation feedback + hint + help + info) lives in `DXFieldShell`; the numeric controls (currency/percentage/number) in `DXNumericField`; the choice controls (select/searchable/radio/checkbox-group) in `DXChoiceField`; and the async option loading in the `useAsyncOptions` composable. This file decides which control to render inside the shell by `field.type`. None of those pieces are exported — they exist only to keep this component factored.",
2122
2122
  "props": [
2123
2123
  {
2124
2124
  "name": "field",
@@ -2317,6 +2317,64 @@
2317
2317
  "name": "repeater-row",
2318
2318
  "description": "Custom layout for a single repeater row, forwarded to `DXRepeater`'s `row` slot with its row props (row index, item, remove handler, etc.).",
2319
2319
  "bindings": []
2320
+ },
2321
+ {
2322
+ "name": "password-actions",
2323
+ "description": "Extra controls beside a password input, after its input group (the input and the reveal (eye) toggle), e.g. a \"Generate password\" button. They sit on the input's row while it keeps `--dx-password-input-min-width`, and wrap below it when the row is too narrow. Use an ordinary button (a `secondary` `DButton` matches the form's other buttons). Not rendered on a `plaintext` field.",
2324
+ "bindings": [
2325
+ {
2326
+ "title": "binding",
2327
+ "type": {
2328
+ "name": "FieldDefinition"
2329
+ },
2330
+ "name": "field",
2331
+ "description": "The field definition being rendered."
2332
+ },
2333
+ {
2334
+ "title": "binding",
2335
+ "type": {
2336
+ "name": "any"
2337
+ },
2338
+ "name": "model",
2339
+ "description": "The model passed to field predicates (defaults to the live form data)."
2340
+ },
2341
+ {
2342
+ "title": "binding",
2343
+ "type": {
2344
+ "name": "any"
2345
+ },
2346
+ "name": "value",
2347
+ "description": "The current field value."
2348
+ },
2349
+ {
2350
+ "title": "binding",
2351
+ "type": {
2352
+ "name": "(value: any) => void"
2353
+ },
2354
+ "name": "update",
2355
+ "description": "Setter that writes the value back and clears the field's validation error."
2356
+ },
2357
+ {
2358
+ "title": "binding",
2359
+ "type": {
2360
+ "name": "boolean"
2361
+ },
2362
+ "name": "revealed",
2363
+ "description": "Whether the password is currently shown as plain text."
2364
+ },
2365
+ {
2366
+ "name": "set-revealed",
2367
+ "title": "binding"
2368
+ },
2369
+ {
2370
+ "title": "binding",
2371
+ "type": {
2372
+ "name": "boolean"
2373
+ },
2374
+ "name": "disabled",
2375
+ "description": "Whether the field is disabled or read-only, so actions that change the value can disable themselves."
2376
+ }
2377
+ ]
2320
2378
  }
2321
2379
  ],
2322
2380
  "methods": []
@@ -2796,7 +2854,7 @@
2796
2854
  "name": "DXFormField",
2797
2855
  "category": "extended",
2798
2856
  "filePath": "resources/js/components/extended/DXFormField.vue",
2799
- "description": "DXFormField — internal per-field renderer for DXForm. Owns the single per-field render block that DXForm's tabbed and flat layouts both need: the `field(<key>)` full-replacement slot, the `field-before(<key>)`/`field-after(<key>)` wrapper slots, and the `<DXField>` element with its `value`/`span`/`info`/`hint`/`repeater-row` slot forwarding. Extracted so those per-field props/slots are wired in ONE place — before this, DXForm carried two near-identical copies (tabbed + flat) and a new per-field prop had to be added to both, which was easy to miss (see #78/#83). DXForm forwards all of its slots into each instance; this component only reads the ones matching its field's key. Internal: not exported from the package index — consumers use DXForm.",
2857
+ "description": "DXFormField — internal per-field renderer for DXForm. Owns the single per-field render block that DXForm's tabbed and flat layouts both need: the `field(<key>)` full-replacement slot, the `field-before(<key>)`/`field-after(<key>)` wrapper slots, and the `<DXField>` element with its `value`/`span`/`info`/`hint`/`repeater-row`/ `switch-list-item`/`password-actions` slot forwarding. Extracted so those per-field props/slots are wired in ONE place — before this, DXForm carried two near-identical copies (tabbed + flat) and a new per-field prop had to be added to both, which was easy to miss (see #78/#83). DXForm forwards all of its slots into each instance; this component only reads the ones matching its field's key. Internal: not exported from the package index — consumers use DXForm.",
2800
2858
  "props": [
2801
2859
  {
2802
2860
  "name": "field",
@@ -4126,7 +4184,7 @@
4126
4184
  "name": "DXTableEditorModal",
4127
4185
  "category": "extended",
4128
4186
  "filePath": "resources/js/components/extended/DXTableEditorModal.vue",
4129
- "description": "Internal edit/create modal for `DXTable` (#129). Renders the `DModal` + `DXForm` and maps DXTable's `edit-value`/`edit-span`/`tab-*` slots onto DXForm's `value`/`span`/`tab-*` slots, providing the row context and a `close` handle. Extracted so `DXTable` is a thin façade over the table render, the pagination, the editor logic (`useResourceEditor`) and this modal — rather than owning all four. The edit-modal slots reach here forwarded from DXTable (consumer → DXTable → here → DXForm). This component computes which keyed slots the consumer actually provided from its OWN `$slots`, so the slot-plumbing lives entirely with the modal, not the composable.",
4187
+ "description": "Internal edit/create modal for `DXTable` (#129). Renders the `DModal` + `DXForm` and maps DXTable's `edit-value`/`edit-span`/`edit-password-actions`/ `tab-*` slots onto DXForm's `value`/`span`/`password-actions`/`tab-*` slots, providing the row context and a `close` handle. Extracted so `DXTable` is a thin façade over the table render, the pagination, the editor logic (`useResourceEditor`) and this modal — rather than owning all four. The edit-modal slots reach here forwarded from DXTable (consumer → DXTable → here → DXForm). This component computes which keyed slots the consumer actually provided from its OWN `$slots`, so the slot-plumbing lives entirely with the modal, not the composable.",
4130
4188
  "props": [
4131
4189
  {
4132
4190
  "name": "show",
@@ -4373,6 +4431,68 @@
4373
4431
  }
4374
4432
  ]
4375
4433
  },
4434
+ {
4435
+ "name": "`edit-password-actions(${key})`",
4436
+ "description": "Extra controls beside password field `<key>`'s input in the edit/create modal, inside its input group after the reveal (eye) toggle — e.g. a \"Generate password\" button. Name it `edit-password-actions(<fieldKey>)`.",
4437
+ "bindings": [
4438
+ {
4439
+ "name": "name",
4440
+ "title": "binding"
4441
+ },
4442
+ {
4443
+ "title": "binding",
4444
+ "type": {
4445
+ "name": "object"
4446
+ },
4447
+ "name": "item",
4448
+ "description": "The row being edited (null in create mode)."
4449
+ },
4450
+ {
4451
+ "title": "binding",
4452
+ "type": {
4453
+ "name": "any"
4454
+ },
4455
+ "name": "value",
4456
+ "description": "The current field value."
4457
+ },
4458
+ {
4459
+ "title": "binding",
4460
+ "type": {
4461
+ "name": "Function"
4462
+ },
4463
+ "name": "update",
4464
+ "description": "Call with a new value to update the field."
4465
+ },
4466
+ {
4467
+ "title": "binding",
4468
+ "type": {
4469
+ "name": "boolean"
4470
+ },
4471
+ "name": "revealed",
4472
+ "description": "Whether the password is currently shown as plain text."
4473
+ },
4474
+ {
4475
+ "name": "set-revealed",
4476
+ "title": "binding"
4477
+ },
4478
+ {
4479
+ "title": "binding",
4480
+ "type": {
4481
+ "name": "boolean"
4482
+ },
4483
+ "name": "disabled",
4484
+ "description": "Whether the field is disabled or read-only."
4485
+ },
4486
+ {
4487
+ "title": "binding",
4488
+ "type": {
4489
+ "name": "object"
4490
+ },
4491
+ "name": "field",
4492
+ "description": "The field definition."
4493
+ }
4494
+ ]
4495
+ },
4376
4496
  {
4377
4497
  "name": "`tab-content(${key})`",
4378
4498
  "description": "Replaces the auto-rendered fields of edit-modal tab `<key>` with custom content. Name it `tab-content(<tabKey>)`.",
@@ -1,7 +1,7 @@
1
1
  # Documentation Map
2
2
 
3
3
  > Auto-generated hierarchical overview of all documentation
4
- > Last updated: 2026-10-15T11:16:01.216Z
4
+ > Last updated: 2026-10-21T10:08:54.546Z
5
5
 
6
6
  This file provides a complete map of all available documentation for AI agents and developers.
7
7
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnitend/dashboard-for-laravel",
3
- "version": "0.46.0",
3
+ "version": "0.47.0",
4
4
  "description": "Vue 3 dashboard components for Laravel with Bootstrap Vue Next",
5
5
  "type": "module",
6
6
  "main": "./dist/dashboard-for-laravel.umd.cjs",
@@ -1644,6 +1644,81 @@ $dx-monospace-elements: "code, kbd, samp, pre";
1644
1644
  font-family: var(--dx-font-family-figures), var(--bs-font-monospace);
1645
1645
  }
1646
1646
 
1647
+ // ----------------------------------------------------------------------------
1648
+ // DXField password fields: the eye as part of the input, actions that wrap
1649
+ // ----------------------------------------------------------------------------
1650
+ //
1651
+ // The reveal (eye) toggle is drawn as part of the field: the input's border
1652
+ // colour, radius and background, a muted icon, and the input's own focus
1653
+ // treatment (navy border + halo). As a plain `outline-secondary` button it was
1654
+ // a darker, square-cornered box stuck to a light rounded input. Driven through
1655
+ // Bootstrap's `--bs-btn-*` variables, so hover/active/focus keep Bootstrap's
1656
+ // resolution order. `.btn.` lifts it over `.btn-outline-secondary`.
1657
+ //
1658
+ // A `password-actions` slot (a Generate button) sits AFTER the input group,
1659
+ // not in it, with a gap: it is an ordinary button beside the field rather than
1660
+ // a third piece of the control. The group asks for room for
1661
+ // `--dx-password-input-min-width` of input plus the square eye; when the row
1662
+ // cannot give the group that AND the actions, the actions wrap onto their own
1663
+ // line below it, so the input never shrinks to a couple of characters. The
1664
+ // group still has `min-width: 0`: in a container narrower than the minimum it
1665
+ // shrinks rather than overflowing.
1666
+ $dx-password-input-min-width: 11rem !default;
1667
+ $dx-password-actions-gap: 0.5rem !default;
1668
+
1669
+ :root {
1670
+ --dx-password-input-min-width: #{$dx-password-input-min-width};
1671
+ }
1672
+
1673
+ .btn.dx-password-reveal {
1674
+ --bs-btn-color: var(--bs-secondary-color);
1675
+ --bs-btn-bg: #{$input-bg};
1676
+ --bs-btn-border-color: var(--d-input-border-color);
1677
+ --bs-btn-border-radius: #{$input-border-radius};
1678
+ --bs-btn-hover-color: var(--bs-body-color);
1679
+ --bs-btn-hover-bg: var(--bs-tertiary-bg);
1680
+ --bs-btn-hover-border-color: var(--d-input-border-color);
1681
+ --bs-btn-active-color: var(--bs-body-color);
1682
+ --bs-btn-active-bg: var(--bs-tertiary-bg);
1683
+ --bs-btn-active-border-color: var(--d-input-border-color);
1684
+ --bs-btn-focus-box-shadow: #{$input-focus-box-shadow};
1685
+
1686
+ &:focus-visible {
1687
+ border-color: $input-focus-border-color;
1688
+ }
1689
+ }
1690
+
1691
+ // Disabled with its field: the disabled input's fill and border, not the
1692
+ // neutral outline-button look (`.btn:is(:disabled)` below), so the eye still
1693
+ // reads as part of the greyed-out control. `.input-group >` outranks that rule.
1694
+ .input-group > .btn.dx-password-reveal:is(:disabled, .disabled),
1695
+ fieldset:disabled .input-group > .btn.dx-password-reveal {
1696
+ --bs-btn-disabled-bg: #{$input-disabled-bg};
1697
+ --bs-btn-disabled-color: var(--bs-secondary-color);
1698
+ --bs-btn-disabled-border-color: var(--d-input-border-color);
1699
+ }
1700
+
1701
+ .dx-password-field {
1702
+ display: flex;
1703
+ flex-wrap: wrap;
1704
+ align-items: flex-start;
1705
+ gap: $dx-password-actions-gap;
1706
+
1707
+ > .dx-password-field__group {
1708
+ // The eye is an icon-only square, `--dx-input-height` wide.
1709
+ flex: 1 1 calc(var(--dx-password-input-min-width) + var(--dx-input-height));
1710
+ min-width: 0;
1711
+ }
1712
+ }
1713
+
1714
+ .dx-password-field__actions {
1715
+ display: flex;
1716
+ flex-wrap: wrap;
1717
+ gap: $dx-password-actions-gap;
1718
+ min-width: 0;
1719
+ max-width: 100%;
1720
+ }
1721
+
1647
1722
  // ----------------------------------------------------------------------------
1648
1723
  // DXCurrencyInput — keep the amount usable in a narrow container
1649
1724
  // ----------------------------------------------------------------------------
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  @component
3
- Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `info-popover`, `hint` and `repeater-row` slots for per-field customisation.
3
+ Renders a single `FieldDefinition` of any type (text, select, autocomplete, checkbox, switch, repeater, currency, percentage, file/image, component, etc.) with its label, validation error, hint and help. Used by `DXForm` and `DXTable`'s edit modal; exposes `value`, `span`, `info`, `info-popover`, `hint`, `repeater-row` and `password-actions` slots for per-field customisation.
4
4
 
5
5
  Structure (#135): a thin DISPATCHER. The shared chrome (label + validation
6
6
  feedback + hint + help + info) lives in `DXFieldShell`; the numeric controls
@@ -336,28 +336,61 @@
336
336
  <!-- Password with a reveal toggle (the default for
337
337
  `type: "password"`). Typing a long password blind is the one
338
338
  place the eye really earns its keep, so it's opt-out
339
- (`revealable: false`) rather than opt-in. -->
340
- <DInputGroup v-else-if="isRevealablePassword">
341
- <DFormInput
342
- v-model="textFieldModel"
343
- :required="field.required"
344
- :placeholder="field.placeholder"
345
- :state="fieldState"
346
- :disabled="isDisabled"
347
- :readonly="isReadonly"
348
- v-bind="passwordInputProps"
349
- />
350
- <template #append>
351
- <DButton
352
- variant="outline-secondary"
353
- :icon="passwordRevealed ? 'eye-slash' : 'eye'"
354
- :aria-label="passwordRevealed ? 'Hide password' : 'Show password'"
355
- :aria-pressed="passwordRevealed"
339
+ (`revealable: false`) rather than opt-in. The eye is drawn
340
+ as part of the field (`.dx-password-reveal`: the input's
341
+ border, radius and background). A `password-actions` slot
342
+ lands AFTER the input group, in `.dx-password-field__actions`,
343
+ which wraps below the group when the row cannot give the
344
+ input `--dx-password-input-min-width` (theme.scss); it also
345
+ brings the group to a `revealable: false` field. -->
346
+ <div v-else-if="isPasswordGroup()" class="dx-password-field">
347
+ <DInputGroup class="dx-password-field__group">
348
+ <DFormInput
349
+ v-model="textFieldModel"
350
+ :required="field.required"
351
+ :placeholder="field.placeholder"
352
+ :state="fieldState"
356
353
  :disabled="isDisabled"
357
- @click="passwordRevealed = !passwordRevealed"
354
+ :readonly="isReadonly"
355
+ v-bind="passwordInputProps"
358
356
  />
359
- </template>
360
- </DInputGroup>
357
+ <template #append>
358
+ <DButton
359
+ v-if="isRevealablePassword"
360
+ class="dx-password-reveal"
361
+ variant="outline-secondary"
362
+ icon-only
363
+ :icon="passwordRevealed ? 'eye-slash' : 'eye'"
364
+ :aria-label="passwordRevealed ? 'Hide password' : 'Show password'"
365
+ :aria-pressed="passwordRevealed"
366
+ :disabled="isDisabled"
367
+ @click="passwordRevealed = !passwordRevealed"
368
+ />
369
+ </template>
370
+ </DInputGroup>
371
+ <div v-if="slots['password-actions'] !== undefined" class="dx-password-field__actions">
372
+ <!--
373
+ @slot Extra controls beside a password input, after its input group (the input and the reveal (eye) toggle), e.g. a "Generate password" button. They sit on the input's row while it keeps `--dx-password-input-min-width`, and wrap below it when the row is too narrow. Use an ordinary button (a `secondary` `DButton` matches the form's other buttons). Not rendered on a `plaintext` field.
374
+ @binding {FieldDefinition} field The field definition being rendered.
375
+ @binding {any} model The model passed to field predicates (defaults to the live form data).
376
+ @binding {any} value The current field value.
377
+ @binding {(value: any) => void} update Setter that writes the value back and clears the field's validation error.
378
+ @binding {boolean} revealed Whether the password is currently shown as plain text.
379
+ @binding {(revealed: boolean) => void} setRevealed Show (`true`) or mask (`false`) the password, exactly as the eye does. A no-op on a `revealable: false` field.
380
+ @binding {boolean} disabled Whether the field is disabled or read-only, so actions that change the value can disable themselves.
381
+ -->
382
+ <slot
383
+ name="password-actions"
384
+ :field="field"
385
+ :model="model"
386
+ :value="fieldValue"
387
+ :update="setValue"
388
+ :revealed="passwordRevealed"
389
+ :set-revealed="setPasswordRevealed"
390
+ :disabled="isDisabled || isReadonly"
391
+ />
392
+ </div>
393
+ </div>
361
394
 
362
395
  <!-- Text-based inputs (text/email/password/url/tel/date/time/datetime) -->
363
396
  <DFormInput
@@ -819,6 +852,32 @@ const isRevealablePassword = computed(
819
852
  !isPlaintext.value,
820
853
  );
821
854
 
855
+ /*
856
+ * The password input group: the eye (unless `revealable: false`) plus any
857
+ * `password-actions` slot content. A `revealable: false` field with no slot
858
+ * stays a bare input, as before; with the slot it gets the group so the
859
+ * actions have somewhere to sit, but still no eye and no way to unmask.
860
+ * A function, not a computed: `useSlots()` is not reactive, so a computed
861
+ * would keep the first render's answer if the slot came or went later.
862
+ */
863
+ function isPasswordGroup(): boolean {
864
+ return (
865
+ isRevealablePassword.value ||
866
+ (props.field.type === "password" &&
867
+ !isPlaintext.value &&
868
+ slots["password-actions"] !== undefined)
869
+ );
870
+ }
871
+
872
+ /**
873
+ * The `password-actions` slot's handle on the reveal state: the same state
874
+ * the eye toggles, so the two never disagree. Refused on a `revealable: false`
875
+ * field, which promises never to unmask.
876
+ */
877
+ function setPasswordRevealed(revealed: boolean): void {
878
+ passwordRevealed.value = isRevealablePassword.value && revealed === true;
879
+ }
880
+
822
881
  // DXTable reuses ONE DXField instance per field key across modal opens (the
823
882
  // v-for is keyed on `field.key`, which is stable across rows) and swaps in a
824
883
  // fresh form per record. Without this, revealing row A's password leaves row
@@ -864,10 +923,14 @@ const controlPropsWithGuards = computed(() => {
864
923
  return merged;
865
924
  });
866
925
 
867
- // The reveal toggle owns the password input's `type`.
926
+ // The reveal toggle owns the password input's `type`. Only a revealable field
927
+ // can be shown as text (`setPasswordRevealed` also refuses the others).
868
928
  const passwordInputProps = computed(() => ({
869
929
  ...inputPropsWithGuards.value,
870
- type: passwordRevealed.value ? "text" : "password",
930
+ type:
931
+ passwordRevealed.value && isRevealablePassword.value
932
+ ? "text"
933
+ : "password",
871
934
  }));
872
935
 
873
936
  const inputType = computed<string>(() => {
@@ -5,7 +5,8 @@
5
5
  Owns the single per-field render block that DXForm's tabbed and flat layouts
6
6
  both need: the `field(<key>)` full-replacement slot, the
7
7
  `field-before(<key>)`/`field-after(<key>)` wrapper slots, and the `<DXField>`
8
- element with its `value`/`span`/`info`/`hint`/`repeater-row` slot forwarding.
8
+ element with its `value`/`span`/`info`/`hint`/`repeater-row`/
9
+ `switch-list-item`/`password-actions` slot forwarding.
9
10
 
10
11
  Extracted so those per-field props/slots are wired in ONE place — before this,
11
12
  DXForm carried two near-identical copies (tabbed + flat) and a new per-field
@@ -99,6 +100,7 @@ function fieldSlotMap(): Record<string, string> {
99
100
  ["hint", `hint(${key})`],
100
101
  ["repeater-row", `repeater-row(${key})`],
101
102
  ["switch-list-item", `switch-list-item(${key})`],
103
+ ["password-actions", `password-actions(${key})`],
102
104
  ];
103
105
  for (const [target, source] of candidates) {
104
106
  if (slots[source]) map[target] = source;
@@ -266,7 +266,8 @@
266
266
  </DRow>
267
267
 
268
268
  <!-- Edit/create modal (only when editFields provided). The consumer's
269
- edit-value / edit-span / tab-* slots are forwarded through to it
269
+ edit-value / edit-span / edit-password-actions / tab-* slots are
270
+ forwarded through to it
270
271
  generically; it maps them onto DXForm and provides the row context
271
272
  + a close handle (#129). -->
272
273
  <DXTableEditorModal
@@ -299,6 +300,7 @@
299
300
  >
300
301
  <!--
301
302
  @slot edit-value(<fieldKey>) / edit-span(<fieldKey>) /
303
+ edit-password-actions(<fieldKey>) /
302
304
  tab-content|tab-before|tab-after(<tabKey>) — forwarded to the edit
303
305
  modal (see DXTableEditorModal for their bindings).
304
306
  -->
@@ -2514,6 +2516,7 @@ const handleDelete = editor.remove;
2514
2516
  const EDIT_MODAL_SLOT_PREFIXES = [
2515
2517
  'edit-value(',
2516
2518
  'edit-span(',
2519
+ 'edit-password-actions(',
2517
2520
  'tab-content(',
2518
2521
  'tab-before(',
2519
2522
  'tab-after(',
@@ -1,8 +1,8 @@
1
1
  <!--
2
2
  @component
3
3
  Internal edit/create modal for `DXTable` (#129). Renders the `DModal` +
4
- `DXForm` and maps DXTable's `edit-value`/`edit-span`/`tab-*` slots onto
5
- DXForm's `value`/`span`/`tab-*` slots, providing the row context and a `close`
4
+ `DXForm` and maps DXTable's `edit-value`/`edit-span`/`edit-password-actions`/
5
+ `tab-*` slots onto DXForm's `value`/`span`/`password-actions`/`tab-*` slots, providing the row context and a `close`
6
6
  handle. Extracted so `DXTable` is a thin façade over the table render, the
7
7
  pagination, the editor logic (`useResourceEditor`) and this modal — rather than
8
8
  owning all four.
@@ -115,6 +115,36 @@
115
115
  />
116
116
  </template>
117
117
 
118
+ <!-- Forward edit-password-actions(key) → password-actions(key):
119
+ content beside the password input, inside its input group,
120
+ with a handle on dfl's own reveal (eye) state. -->
121
+ <template
122
+ v-for="key in editPasswordActionsSlotKeys"
123
+ :key="`epa-${key}`"
124
+ #[`password-actions(${key})`]="sp"
125
+ >
126
+ <!--
127
+ @slot Extra controls beside password field `<key>`'s input in the edit/create modal, inside its input group after the reveal (eye) toggle — e.g. a "Generate password" button. Name it `edit-password-actions(<fieldKey>)`.
128
+ @binding {object} item The row being edited (null in create mode).
129
+ @binding {any} value The current field value.
130
+ @binding {Function} update Call with a new value to update the field.
131
+ @binding {boolean} revealed Whether the password is currently shown as plain text.
132
+ @binding {Function} setRevealed Call with true to show the password (as the eye does) or false to mask it.
133
+ @binding {boolean} disabled Whether the field is disabled or read-only.
134
+ @binding {object} field The field definition.
135
+ -->
136
+ <slot
137
+ :name="`edit-password-actions(${key})`"
138
+ :item="item"
139
+ :value="sp.value"
140
+ :update="sp.update"
141
+ :revealed="sp.revealed"
142
+ :set-revealed="sp.setRevealed"
143
+ :disabled="sp.disabled"
144
+ :field="sp.field"
145
+ />
146
+ </template>
147
+
118
148
  <!-- Forward tab-content / tab-before / tab-after slots -->
119
149
  <template
120
150
  v-for="key in tabContentSlotKeys"
@@ -318,6 +348,9 @@ const editValueSlotKeys = computed(() =>
318
348
  const editSpanSlotKeys = computed(() =>
319
349
  fieldKeys.value.filter((key) => !!slots[`edit-span(${key})`]),
320
350
  );
351
+ const editPasswordActionsSlotKeys = computed(() =>
352
+ fieldKeys.value.filter((key) => !!slots[`edit-password-actions(${key})`]),
353
+ );
321
354
  const tabContentSlotKeys = computed(() =>
322
355
  tabKeys.value.filter((key) => !!slots[`tab-content(${key})`]),
323
356
  );