@stsdti/approvable-vue 0.1.7 → 0.1.12

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.
@@ -0,0 +1,135 @@
1
+ <template>
2
+ <div v-if="hasActions" class="approvable-step-actions">
3
+ <div class="approvable-btn-group">
4
+ <!-- APPROVABLE BUTTONS (custom renderer per button, else basic) -->
5
+ <template v-for="(button, index) in step.buttons" :key="button.id ?? index">
6
+ <component
7
+ :is="buttonRenderer(button)"
8
+ v-if="buttonRenderer(button)"
9
+ :action-ui="button.action_ui"
10
+ :approvable="approvable"
11
+ :button="button"
12
+ :buttons="[button]"
13
+ :complete="onApproveStatusChange"
14
+ :resource="resource"
15
+ :step="value"
16
+ @is-loading="(isLoading) => emits('isLoading', isLoading)"
17
+ @refresh="emits('refresh')"
18
+ />
19
+ <component
20
+ :is="linkComponent"
21
+ v-else-if="button.link"
22
+ v-bind="linkProps(linkComponent, button.link)"
23
+ >
24
+ <button
25
+ :title="button.tooltip"
26
+ :class="buttonClass(button)"
27
+ type="button"
28
+ @click="onApproveStatusChange(button)"
29
+ >
30
+ <approvable-icon :icon="button.icon" />
31
+ </button>
32
+ </component>
33
+ <button
34
+ v-else
35
+ :title="button.tooltip"
36
+ :class="buttonClass(button)"
37
+ type="button"
38
+ @click="onApproveStatusChange(button)"
39
+ >
40
+ <approvable-icon :icon="button.icon" />
41
+ {{ button.text }}
42
+ </button>
43
+ </template>
44
+ </div>
45
+
46
+ <div v-if="value.can_comment" class="approvable-btn-group">
47
+ <approvable-step-comment
48
+ :approvable-step="value"
49
+ :disabled="!value.can_comment"
50
+ @refresh="emits('refresh')"
51
+ @is-loading="(isLoading) => emits('isLoading', isLoading)"
52
+ />
53
+ </div>
54
+ </div>
55
+ </template>
56
+
57
+ <script setup>
58
+ import { computed } from 'vue'
59
+ import { createStepViewModel, performStepAction } from '@stsdti/approvable-core'
60
+ import ApprovableIcon from './ApprovableIcon.vue'
61
+ import ApprovableStepComment from './ApprovableStepComment.vue'
62
+ import { approvableActionRenderer } from './action-renderers.js'
63
+ import { linkProps, useLinkComponent } from '../../config.js'
64
+
65
+ /**
66
+ * The controls a step offers — its workflow buttons and, where allowed, the
67
+ * comment popover.
68
+ *
69
+ * Extracted from the step row because the compact form renders exactly these
70
+ * controls beside a one-line summary instead of beside a trail. Two copies of
71
+ * this markup would be two places for a custom action renderer, a navigating
72
+ * button or the refresh-after-action rule to quietly diverge.
73
+ */
74
+ const emits = defineEmits(['isLoading', 'refresh'])
75
+
76
+ const props = defineProps({
77
+ value: {
78
+ type: Object,
79
+ required: true
80
+ },
81
+ approvable: {
82
+ type: Object,
83
+ required: true
84
+ },
85
+ resource: {
86
+ type: Object,
87
+ required: true
88
+ }
89
+ })
90
+
91
+ const linkComponent = useLinkComponent()
92
+
93
+ const step = computed(() => createStepViewModel(props.value))
94
+
95
+ /**
96
+ * A row shows controls when it has any — a finished step usually has none, and
97
+ * shows when it was decided instead.
98
+ */
99
+ const hasActions = computed(() => step.value.buttons.length > 0 || !!props.value.can_comment)
100
+
101
+ const buttonClass = (button) => ({
102
+ 'approvable-btn': true,
103
+ // A button carrying only an icon collapses to a circle, so the row keeps its
104
+ // rhythm instead of stretching around an empty label.
105
+ 'approvable-btn--icon': !button.text,
106
+ 'approvable-btn--primary': button.result === 'approved'
107
+ })
108
+
109
+ const buttonRenderer = (button) => {
110
+ if (!step.value.isActive) {
111
+ return null
112
+ }
113
+
114
+ return approvableActionRenderer(button?.action_ui)
115
+ }
116
+
117
+ const onApproveStatusChange = async (buttonOrId) => {
118
+ emits('isLoading', true)
119
+
120
+ const result = await performStepAction({
121
+ stepId: step.value.id,
122
+ button: buttonOrId,
123
+ buttons: step.value.buttons
124
+ })
125
+
126
+ emits('isLoading', false)
127
+
128
+ // Refresh after any attempted request, success or failure — a failed action
129
+ // still means the local state may be stale. Only a skipped one (no id, no
130
+ // button, or a presentational button) reloads nothing, since it sent nothing.
131
+ if (!result.skipped) {
132
+ emits('refresh')
133
+ }
134
+ }
135
+ </script>
package/src/index.js CHANGED
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
  import Approvable from './components/approvable/Approvable.vue'
14
+ import ApprovableCompact from './components/approvable/ApprovableCompact.vue'
14
15
  import ApprovableStep from './components/approvable/ApprovableStep.vue'
16
+ import ApprovableStepActions from './components/approvable/ApprovableStepActions.vue'
15
17
  import ApprovableButtons from './components/approvable/ApprovableButtons.vue'
16
18
  import ApprovableStepComment from './components/approvable/ApprovableStepComment.vue'
17
19
  import ApprovableAuditTimeline from './components/approvable/ApprovableAuditTimeline.vue'
@@ -31,7 +33,9 @@ import {
31
33
 
32
34
  export {
33
35
  Approvable,
36
+ ApprovableCompact,
34
37
  ApprovableStep,
38
+ ApprovableStepActions,
35
39
  ApprovableButtons,
36
40
  ApprovableStepComment,
37
41
  ApprovableAuditTimeline,
@@ -80,7 +84,9 @@ export {
80
84
 
81
85
  const components = {
82
86
  Approvable,
87
+ ApprovableCompact,
83
88
  ApprovableStep,
89
+ ApprovableStepActions,
84
90
  ApprovableButtons,
85
91
  ApprovableStepComment,
86
92
  ApprovableAuditTimeline,
@@ -266,6 +266,29 @@
266
266
  text-underline-offset: 3px;
267
267
  }
268
268
 
269
+ /* The control that folds a long trail down to `visibleStepRange`. A quiet
270
+ outline pill rather than a second header link: it changes what the panel
271
+ shows, where History opens something, and the difference should be legible
272
+ before it is read. */
273
+ .approvable-steps-toggle-btn {
274
+ flex: none;
275
+ padding: 4px 10px;
276
+ border: 1px solid var(--approvable-control-border);
277
+ border-radius: 999px;
278
+ background: var(--approvable-control);
279
+ color: var(--approvable-control-ink);
280
+ font-family: inherit;
281
+ font-size: 11px;
282
+ font-weight: 600;
283
+ line-height: 1.2;
284
+ white-space: nowrap;
285
+ cursor: pointer;
286
+ }
287
+
288
+ .approvable-steps-toggle-btn:hover {
289
+ background: var(--approvable-control-hover);
290
+ }
291
+
269
292
  /* --------------------------------------------------------------------------
270
293
  Status badge — the header pill, and anywhere else a status is named
271
294
  -------------------------------------------------------------------------- */
@@ -337,6 +360,161 @@
337
360
  50% { opacity: 1; transform: scale(1); }
338
361
  }
339
362
 
363
+ /* --------------------------------------------------------------------------
364
+ Compact — the collapsed, one-line form
365
+
366
+ Status, a dot per step, who it is waiting on and that step's controls, on a
367
+ single row. Built for a table cell or a page header, where the full trail
368
+ does not fit and its absence is the point.
369
+ -------------------------------------------------------------------------- */
370
+ .approvable-compact {
371
+ display: flex;
372
+ align-items: center;
373
+ gap: 11px;
374
+ padding: 10px 12px;
375
+ }
376
+
377
+ .approvable-compact-main {
378
+ flex: 1;
379
+ min-width: 0;
380
+ display: flex;
381
+ flex-direction: column;
382
+ gap: 3px;
383
+ }
384
+
385
+ .approvable-compact-line {
386
+ display: flex;
387
+ align-items: center;
388
+ gap: 7px;
389
+ min-width: 0;
390
+ }
391
+
392
+ /* The whole chain, one dot per visible step, in workflow order. Small enough
393
+ to sit beside the badge without competing with it, and the only place the
394
+ collapsed form admits how long the approval actually is. */
395
+ .approvable-compact-dots {
396
+ display: flex;
397
+ align-items: center;
398
+ gap: 3px;
399
+ flex: none;
400
+ }
401
+
402
+ .approvable-compact-dot {
403
+ width: 5px;
404
+ height: 5px;
405
+ flex: none;
406
+ border-radius: 999px;
407
+ background: var(--approvable-neutral-tint);
408
+ }
409
+
410
+ .approvable-compact-dot--success {
411
+ background: var(--approvable-success-text);
412
+ }
413
+
414
+ .approvable-compact-dot--danger {
415
+ background: var(--approvable-danger-text);
416
+ }
417
+
418
+ .approvable-compact-dot--muted {
419
+ background: var(--approvable-muted-soft);
420
+ }
421
+
422
+ .approvable-compact-dot--pending {
423
+ background: var(--approvable-control-border);
424
+ }
425
+
426
+ /* The step being waited on is larger and pulses — the same beat the header's
427
+ waiting badge keeps, so the two read as one signal rather than two. */
428
+ .approvable-compact-dot--active {
429
+ width: 7px;
430
+ height: 7px;
431
+ background: var(--approvable-accent);
432
+ animation: approvable-pulse 1.6s ease-in-out infinite;
433
+ }
434
+
435
+ .approvable-compact-who {
436
+ margin: 0;
437
+ min-width: 0;
438
+ font-size: 11.5px;
439
+ font-weight: 400;
440
+ line-height: 1.35;
441
+ color: var(--approvable-muted);
442
+ white-space: nowrap;
443
+ overflow: hidden;
444
+ text-overflow: ellipsis;
445
+ }
446
+
447
+ .approvable-compact-name {
448
+ font-weight: 600;
449
+ color: var(--approvable-ink);
450
+ }
451
+
452
+ .approvable-compact-sep {
453
+ margin: 0 5px;
454
+ }
455
+
456
+ .approvable-compact-side {
457
+ flex: none;
458
+ display: flex;
459
+ align-items: center;
460
+ gap: 6px;
461
+ }
462
+
463
+ /* The row's own actions sit flush with the chevron rather than in the padded
464
+ block a step row gives them. */
465
+ .approvable-compact-side .approvable-step-actions {
466
+ margin: 0;
467
+ padding: 0;
468
+ }
469
+
470
+ /* The control that swaps the two forms. A circle, so it reads as a disclosure
471
+ rather than as one more thing to approve. */
472
+ .approvable-compact-toggle {
473
+ flex: none;
474
+ width: 26px;
475
+ height: 26px;
476
+ display: inline-flex;
477
+ align-items: center;
478
+ justify-content: center;
479
+ padding: 0;
480
+ border: 1px solid var(--approvable-control-border);
481
+ border-radius: 999px;
482
+ background: var(--approvable-control);
483
+ color: var(--approvable-control-ink);
484
+ cursor: pointer;
485
+ }
486
+
487
+ .approvable-compact-toggle:hover {
488
+ background: var(--approvable-control-hover);
489
+ }
490
+
491
+ .approvable-compact-toggle:focus-visible {
492
+ outline: 2px solid var(--approvable-accent);
493
+ outline-offset: 2px;
494
+ }
495
+
496
+ /* Same chevron either way; the direction is the state. */
497
+ .approvable-compact-toggle--expanded svg {
498
+ transform: rotate(180deg);
499
+ }
500
+
501
+ /* Narrow enough that the name and its controls cannot share a line, so the
502
+ controls drop below and keep their hit targets. */
503
+ @media (max-width: 420px) {
504
+ .approvable-compact {
505
+ align-items: flex-start;
506
+ flex-wrap: wrap;
507
+ }
508
+
509
+ /* The controls wrap onto their own rows here, so the chevron sits with the
510
+ first of them rather than floating between two. */
511
+ .approvable-compact-side {
512
+ width: 100%;
513
+ align-items: flex-start;
514
+ justify-content: flex-end;
515
+ }
516
+ }
517
+
340
518
  /* --------------------------------------------------------------------------
341
519
  Step rows
342
520
  -------------------------------------------------------------------------- */
@@ -533,18 +711,6 @@
533
711
  display: none;
534
712
  }
535
713
 
536
- /* --------------------------------------------------------------------------
537
- Footer
538
- -------------------------------------------------------------------------- */
539
- .approvable-footer {
540
- padding: 10px 15px 13px;
541
- border-top: 1px solid var(--approvable-divider);
542
- font-size: 11.5px;
543
- font-weight: 400;
544
- line-height: 1.3;
545
- color: var(--approvable-muted);
546
- }
547
-
548
714
  /* --------------------------------------------------------------------------
549
715
  Buttons
550
716
  -------------------------------------------------------------------------- */
@@ -575,6 +741,7 @@
575
741
  .approvable-btn:focus-visible,
576
742
  .approvable-history-btn:focus-visible,
577
743
  .approvable-step-comment-more:focus-visible,
744
+ .approvable-steps-toggle-btn:focus-visible,
578
745
  .approvable-timeline-refresh:focus-visible,
579
746
  .approvable-dialog-close:focus-visible {
580
747
  outline: 2px solid var(--approvable-accent);