@shamar/adonis 0.1.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.
Files changed (134) hide show
  1. package/assets/admin.css +2 -0
  2. package/assets/alpine.min.js +5 -0
  3. package/assets/input.css +2286 -0
  4. package/assets/shamar-ui.js +3658 -0
  5. package/dist/assets-controller.d.ts +12 -0
  6. package/dist/assets-controller.d.ts.map +1 -0
  7. package/dist/assets-controller.js +32 -0
  8. package/dist/assets-controller.js.map +1 -0
  9. package/dist/config.d.ts +79 -0
  10. package/dist/config.d.ts.map +1 -0
  11. package/dist/config.js +38 -0
  12. package/dist/config.js.map +1 -0
  13. package/dist/configure.d.ts +21 -0
  14. package/dist/configure.d.ts.map +1 -0
  15. package/dist/configure.js +20 -0
  16. package/dist/configure.js.map +1 -0
  17. package/dist/context.d.ts +22 -0
  18. package/dist/context.d.ts.map +1 -0
  19. package/dist/context.js +2 -0
  20. package/dist/context.js.map +1 -0
  21. package/dist/controller.d.ts +31 -0
  22. package/dist/controller.d.ts.map +1 -0
  23. package/dist/controller.js +45 -0
  24. package/dist/controller.js.map +1 -0
  25. package/dist/controllers/admin_controller.d.ts +72 -0
  26. package/dist/controllers/admin_controller.d.ts.map +1 -0
  27. package/dist/controllers/admin_controller.js +1075 -0
  28. package/dist/controllers/admin_controller.js.map +1 -0
  29. package/dist/discover.d.ts +7 -0
  30. package/dist/discover.d.ts.map +1 -0
  31. package/dist/discover.js +50 -0
  32. package/dist/discover.js.map +1 -0
  33. package/dist/form-state.d.ts +41 -0
  34. package/dist/form-state.d.ts.map +1 -0
  35. package/dist/form-state.js +74 -0
  36. package/dist/form-state.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +10 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/middleware/require_api_key_middleware.d.ts +30 -0
  42. package/dist/middleware/require_api_key_middleware.d.ts.map +1 -0
  43. package/dist/middleware/require_api_key_middleware.js +59 -0
  44. package/dist/middleware/require_api_key_middleware.js.map +1 -0
  45. package/dist/provider.d.ts +9 -0
  46. package/dist/provider.d.ts.map +1 -0
  47. package/dist/provider.js +66 -0
  48. package/dist/provider.js.map +1 -0
  49. package/dist/resources/api_key_resource.d.ts +35 -0
  50. package/dist/resources/api_key_resource.d.ts.map +1 -0
  51. package/dist/resources/api_key_resource.js +225 -0
  52. package/dist/resources/api_key_resource.js.map +1 -0
  53. package/dist/routes.d.ts +5 -0
  54. package/dist/routes.d.ts.map +1 -0
  55. package/dist/routes.js +97 -0
  56. package/dist/routes.js.map +1 -0
  57. package/dist/runtime.d.ts +27 -0
  58. package/dist/runtime.d.ts.map +1 -0
  59. package/dist/runtime.js +74 -0
  60. package/dist/runtime.js.map +1 -0
  61. package/dist/shamar/auth.d.ts +30 -0
  62. package/dist/shamar/auth.d.ts.map +1 -0
  63. package/dist/shamar/auth.js +218 -0
  64. package/dist/shamar/auth.js.map +1 -0
  65. package/dist/shamar/branding.d.ts +24 -0
  66. package/dist/shamar/branding.d.ts.map +1 -0
  67. package/dist/shamar/branding.js +91 -0
  68. package/dist/shamar/branding.js.map +1 -0
  69. package/dist/shamar/display-relations.d.ts +7 -0
  70. package/dist/shamar/display-relations.d.ts.map +1 -0
  71. package/dist/shamar/display-relations.js +89 -0
  72. package/dist/shamar/display-relations.js.map +1 -0
  73. package/dist/shamar/list-headers.d.ts +38 -0
  74. package/dist/shamar/list-headers.d.ts.map +1 -0
  75. package/dist/shamar/list-headers.js +218 -0
  76. package/dist/shamar/list-headers.js.map +1 -0
  77. package/dist/shamar/list-query.d.ts +229 -0
  78. package/dist/shamar/list-query.d.ts.map +1 -0
  79. package/dist/shamar/list-query.js +564 -0
  80. package/dist/shamar/list-query.js.map +1 -0
  81. package/dist/shamar/menu.d.ts +48 -0
  82. package/dist/shamar/menu.d.ts.map +1 -0
  83. package/dist/shamar/menu.js +93 -0
  84. package/dist/shamar/menu.js.map +1 -0
  85. package/dist/shamar/paths.d.ts +5 -0
  86. package/dist/shamar/paths.d.ts.map +1 -0
  87. package/dist/shamar/paths.js +16 -0
  88. package/dist/shamar/paths.js.map +1 -0
  89. package/dist/shamar/relation-fields.d.ts +54 -0
  90. package/dist/shamar/relation-fields.d.ts.map +1 -0
  91. package/dist/shamar/relation-fields.js +94 -0
  92. package/dist/shamar/relation-fields.js.map +1 -0
  93. package/dist/shamar/resource-actions.d.ts +9 -0
  94. package/dist/shamar/resource-actions.d.ts.map +1 -0
  95. package/dist/shamar/resource-actions.js +42 -0
  96. package/dist/shamar/resource-actions.js.map +1 -0
  97. package/dist/shamar/view-context.d.ts +79 -0
  98. package/dist/shamar/view-context.d.ts.map +1 -0
  99. package/dist/shamar/view-context.js +74 -0
  100. package/dist/shamar/view-context.js.map +1 -0
  101. package/dist/types.d.ts +15 -0
  102. package/dist/types.d.ts.map +1 -0
  103. package/dist/types.js +2 -0
  104. package/dist/types.js.map +1 -0
  105. package/package.json +67 -0
  106. package/resources/views/shamar/dashboard.edge +24 -0
  107. package/resources/views/shamar/form.edge +87 -0
  108. package/resources/views/shamar/index.edge +149 -0
  109. package/resources/views/shamar/partials/boolean-value.edge +25 -0
  110. package/resources/views/shamar/partials/csrf.edge +1 -0
  111. package/resources/views/shamar/partials/detail-entry.edge +72 -0
  112. package/resources/views/shamar/partials/dialog-host.edge +125 -0
  113. package/resources/views/shamar/partials/field-affix-icon.edge +23 -0
  114. package/resources/views/shamar/partials/field-input.edge +585 -0
  115. package/resources/views/shamar/partials/field-relation-checkbox.edge +104 -0
  116. package/resources/views/shamar/partials/field-relation-m2m.edge +110 -0
  117. package/resources/views/shamar/partials/field-relation-m2o.edge +116 -0
  118. package/resources/views/shamar/partials/field-relation-table.edge +139 -0
  119. package/resources/views/shamar/partials/form-container.edge +82 -0
  120. package/resources/views/shamar/partials/icon.edge +14 -0
  121. package/resources/views/shamar/partials/list-row.edge +73 -0
  122. package/resources/views/shamar/partials/list-toolbar.edge +269 -0
  123. package/resources/views/shamar/partials/nav-mobile.edge +21 -0
  124. package/resources/views/shamar/partials/nav.edge +58 -0
  125. package/resources/views/shamar/partials/page-heading.edge +130 -0
  126. package/resources/views/shamar/partials/pagination.edge +73 -0
  127. package/resources/views/shamar/partials/record-pager.edge +37 -0
  128. package/resources/views/shamar/partials/schema-node.edge +289 -0
  129. package/resources/views/shamar/partials/shell-close.edge +20 -0
  130. package/resources/views/shamar/partials/shell-open.edge +51 -0
  131. package/resources/views/shamar/partials/toast.edge +5 -0
  132. package/resources/views/shamar/partials/topbar.edge +115 -0
  133. package/resources/views/shamar/show.edge +17 -0
  134. package/stubs/config/shamar.stub +29 -0
@@ -0,0 +1,104 @@
1
+ {{-- Checkbox list for manyToMany. Expects `relationConfig`. --}}
2
+ <div
3
+ class="shamar-m2m-checkboxes relative w-full"
4
+ data-shamar-m2m-config="{{ jsonAttr(relationConfig) }}"
5
+ x-data="shamarM2mCheckboxFromEl($el)"
6
+ >
7
+ <template x-if="!readonly">
8
+ <template x-for="item in selected" :key="'h-' + itemValue(item)">
9
+ <input type="hidden" name="{{ relationConfig.name }}[]" :value="itemValue(item)" />
10
+ </template>
11
+ </template>
12
+
13
+ <div class="shamar-m2m-checkboxes__toolbar mb-2 flex items-center gap-2" x-show="!readonly">
14
+ <input
15
+ type="search"
16
+ x-model="filter"
17
+ placeholder="Filter…"
18
+ class="shamar-input w-full max-w-xs text-sm"
19
+ autocomplete="off"
20
+ />
21
+ <span class="shrink-0 text-xs text-body-subtle" x-text="selected.length + ' selected'"></span>
22
+ </div>
23
+
24
+ <div
25
+ class="shamar-m2m-checkboxes__body"
26
+ :class="framed ? 'shamar-m2m-checkboxes__body--framed' : 'shamar-m2m-checkboxes__body--open'"
27
+ >
28
+ <template x-if="loading">
29
+ <div class="px-2 py-3 text-xs text-body-subtle">Loading…</div>
30
+ </template>
31
+ <template x-if="!loading && filteredGroups.length === 0">
32
+ <div class="px-2 py-3 text-xs text-body-subtle">No related records.</div>
33
+ </template>
34
+
35
+ <div
36
+ x-show="!loading && groupBy && filteredGroups.length > 0"
37
+ class="shamar-m2m-checkboxes__clusters"
38
+ :class="'shamar-m2m-checkboxes__clusters--cols-' + columns"
39
+ :style="'--shamar-checkbox-cols:' + columns"
40
+ >
41
+ <template x-for="group in filteredGroups" :key="group.key">
42
+ <section class="shamar-m2m-checkboxes__cluster">
43
+ <header class="shamar-m2m-checkboxes__cluster-header">
44
+ <div class="min-w-0">
45
+ <h4 class="truncate text-xs font-semibold text-heading" x-text="group.title"></h4>
46
+ </div>
47
+ <span
48
+ class="shamar-m2m-checkboxes__cluster-count shrink-0"
49
+ x-text="groupSelectedCount(group) + '/' + group.items.length"
50
+ ></span>
51
+ </header>
52
+ <div class="shamar-m2m-checkboxes__cluster-body">
53
+ <template x-for="item in group.items" :key="item.id">
54
+ <label
55
+ class="shamar-m2m-checkboxes__item flex items-center gap-2 rounded px-1.5 py-1 text-sm"
56
+ :class="{
57
+ 'opacity-50 cursor-not-allowed': isCovered(item) && !isSelected(item),
58
+ 'cursor-pointer hover:bg-neutral-secondary': !readonly && !(isCovered(item) && !isSelected(item)),
59
+ 'opacity-60': readonly
60
+ }"
61
+ >
62
+ <input
63
+ type="checkbox"
64
+ class="shamar-checkbox__input"
65
+ :checked="isSelected(item)"
66
+ :disabled="readonly || (isCovered(item) && !isSelected(item))"
67
+ @change="toggle(item)"
68
+ />
69
+ <span class="min-w-0 truncate text-body" x-text="itemDisplay(item)"></span>
70
+ </label>
71
+ </template>
72
+ </div>
73
+ </section>
74
+ </template>
75
+ </div>
76
+
77
+ <div
78
+ x-show="!loading && !groupBy && filteredOptions.length > 0"
79
+ class="shamar-m2m-checkboxes__list"
80
+ :class="'shamar-m2m-checkboxes__list--cols-' + columns"
81
+ :style="'--shamar-checkbox-cols:' + columns"
82
+ >
83
+ <template x-for="item in filteredOptions" :key="item.id">
84
+ <label
85
+ class="shamar-m2m-checkboxes__item flex items-center gap-2 rounded px-2 py-1.5 text-sm"
86
+ :class="{
87
+ 'opacity-50 cursor-not-allowed': isCovered(item) && !isSelected(item),
88
+ 'cursor-pointer hover:bg-neutral-secondary': !readonly && !(isCovered(item) && !isSelected(item)),
89
+ 'opacity-60': readonly
90
+ }"
91
+ >
92
+ <input
93
+ type="checkbox"
94
+ class="shamar-checkbox__input"
95
+ :checked="isSelected(item)"
96
+ :disabled="readonly || (isCovered(item) && !isSelected(item))"
97
+ @change="toggle(item)"
98
+ />
99
+ <span class="min-w-0 truncate text-body" x-text="itemDisplay(item)"></span>
100
+ </label>
101
+ </template>
102
+ </div>
103
+ </div>
104
+ </div>
@@ -0,0 +1,110 @@
1
+ {{-- ManyToMany / multi relation combobox. Expects `relationConfig`. --}}
2
+ <div
3
+ class="shamar-m2m relative w-full"
4
+ data-shamar-m2m-config="{{ jsonAttr(relationConfig) }}"
5
+ x-data="shamarM2mFromEl($el)"
6
+ @click.outside="close()"
7
+ @focusout="onFocusOut($event)"
8
+ @keydown.escape.prevent.stop="close()"
9
+ >
10
+ <template x-for="item in selected" :key="'h-' + item.id">
11
+ <input type="hidden" name="{{ relationConfig.name }}[]" :value="item.id" />
12
+ </template>
13
+
14
+ <div
15
+ class="shamar-m2m__box shamar-input flex min-h-[2.375rem] w-full flex-wrap items-center gap-1.5 py-1.5 pl-2 pr-2"
16
+ :class="{ 'is-readonly': readonly, 'is-open': open }"
17
+ @click="focusInput()"
18
+ >
19
+ <template x-for="item in selected" :key="item.id">
20
+ <span class="shamar-chip inline-flex max-w-full items-center gap-1 rounded-md bg-brand-softer px-1.5 py-0.5 text-xs text-fg-brand">
21
+ <button
22
+ type="button"
23
+ class="shamar-chip__label max-w-[12rem] truncate hover:underline"
24
+ @click.stop="openRecord(item)"
25
+ x-text="item.label"
26
+ ></button>
27
+ <button
28
+ type="button"
29
+ x-show="!readonly"
30
+ @click.stop="remove(item.id)"
31
+ aria-label="Remove"
32
+ class="shamar-chip__remove rounded p-0.5 text-fg-brand/70 hover:bg-danger-soft hover:text-fg-danger"
33
+ >
34
+ <svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
35
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
36
+ </svg>
37
+ </button>
38
+ </span>
39
+ </template>
40
+
41
+ <input
42
+ type="text"
43
+ x-ref="input"
44
+ x-model="query"
45
+ x-show="!readonly"
46
+ @focus="onFocus()"
47
+ @input.debounce.250ms="onInput()"
48
+ @keydown.arrow-down.prevent="moveCursor(1)"
49
+ @keydown.arrow-up.prevent="moveCursor(-1)"
50
+ @keydown.enter.prevent="onEnter()"
51
+ @keydown.backspace="onBackspace()"
52
+ placeholder="{{ field.placeholder ?? 'Search…' }}"
53
+ autocomplete="off"
54
+ class="shamar-m2m__input min-w-[7rem] flex-1 border-0 bg-transparent p-0 text-sm outline-none focus:ring-0"
55
+ />
56
+ </div>
57
+
58
+ <div
59
+ x-show="open"
60
+ x-cloak
61
+ x-transition.opacity.duration.100ms
62
+ class="shamar-m2m__dropdown absolute left-0 right-0 top-full z-50 mt-1 max-h-72 overflow-auto rounded-md border border-default bg-neutral-primary text-sm shadow-lg"
63
+ role="listbox"
64
+ >
65
+ <template x-if="loading">
66
+ <div class="px-3 py-2 text-xs text-body-subtle">Searching…</div>
67
+ </template>
68
+ <template x-if="!loading && availableResults.length === 0 && !createCandidate">
69
+ <div class="px-3 py-2 text-xs text-body-subtle">No matches.</div>
70
+ </template>
71
+ <ul x-show="!loading && (availableResults.length > 0 || createCandidate || canCreateAndEdit)">
72
+ <template x-for="(item, i) in availableResults" :key="item.id">
73
+ <li
74
+ role="option"
75
+ @mousedown.prevent="pick(item)"
76
+ @mousemove="cursor = i"
77
+ :class="i === cursor ? 'bg-brand-softer text-fg-brand' : 'text-body hover:bg-neutral-secondary'"
78
+ class="cursor-pointer truncate px-3 py-1.5"
79
+ x-text="item.label"
80
+ ></li>
81
+ </template>
82
+ <template x-if="createCandidate">
83
+ <li
84
+ role="option"
85
+ @click="createFromQuery()"
86
+ @mousemove="cursor = availableResults.length"
87
+ :class="cursor === availableResults.length ? 'bg-success-soft text-success-strong' : 'text-fg-success hover:bg-neutral-secondary'"
88
+ class="flex cursor-pointer items-center gap-1.5 border-t border-default px-3 py-1.5"
89
+ >
90
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
91
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
92
+ </svg>
93
+ <span>Create "<span x-text="query"></span>"</span>
94
+ </li>
95
+ </template>
96
+ <template x-if="canCreateAndEdit">
97
+ <li
98
+ role="option"
99
+ @click="createAndEdit()"
100
+ class="flex cursor-pointer items-center gap-1.5 border-t border-default px-3 py-1.5 text-body hover:bg-neutral-secondary"
101
+ >
102
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
103
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z"/>
104
+ </svg>
105
+ <span>Create and edit…</span>
106
+ </li>
107
+ </template>
108
+ </ul>
109
+ </div>
110
+ </div>
@@ -0,0 +1,116 @@
1
+ {{-- BelongsTo async combobox (Create / Create & Edit). Expects `relationConfig`. --}}
2
+ <div
3
+ class="shamar-m2o relative w-full"
4
+ data-shamar-m2o-config="{{ jsonAttr(relationConfig) }}"
5
+ x-data="shamarM2oFromEl($el)"
6
+ @click.outside="close()"
7
+ @focusout="onFocusOut($event)"
8
+ @keydown.escape.prevent.stop="close()"
9
+ >
10
+ <input
11
+ type="hidden"
12
+ name="{{ relationConfig.name }}"
13
+ :value="value === null || value === '' ? '' : value"
14
+ @change="$dispatch('input')"
15
+ />
16
+
17
+ <div class="shamar-m2o__row flex w-full items-stretch gap-1">
18
+ <div class="relative min-w-0 flex-1">
19
+ <input
20
+ type="text"
21
+ x-ref="input"
22
+ x-model="query"
23
+ :readonly="readonly"
24
+ @focus="onFocus()"
25
+ @input.debounce.250ms="onInput()"
26
+ @keydown.arrow-down.prevent="moveCursor(1)"
27
+ @keydown.arrow-up.prevent="moveCursor(-1)"
28
+ @keydown.enter.prevent="onEnter()"
29
+ placeholder="{{ field.placeholder ?? 'Search…' }}"
30
+ autocomplete="off"
31
+ class="shamar-input shamar-m2o__input w-full pr-8"
32
+ :class="{ 'is-readonly': readonly }"
33
+ />
34
+
35
+ <button
36
+ type="button"
37
+ x-show="value !== null && value !== '' && !readonly"
38
+ @click.stop="clearSelection()"
39
+ aria-label="Clear"
40
+ class="shamar-m2o__clear absolute right-1.5 top-1/2 -translate-y-1/2 rounded p-0.5 text-body-subtle hover:bg-danger-soft hover:text-fg-danger"
41
+ >
42
+ <svg class="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5" aria-hidden="true">
43
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12"/>
44
+ </svg>
45
+ </button>
46
+ </div>
47
+
48
+ <button
49
+ type="button"
50
+ x-show="value !== null && value !== ''"
51
+ @click.stop="openRecord()"
52
+ title="Open record"
53
+ aria-label="Open record"
54
+ class="shamar-m2o__open inline-flex h-[2.375rem] w-9 shrink-0 items-center justify-center rounded-md border border-default bg-neutral-primary text-body-subtle hover:border-fg-brand hover:text-fg-brand"
55
+ >
56
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
57
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 6H5.25A2.25 2.25 0 003 8.25v10.5A2.25 2.25 0 005.25 21h10.5A2.25 2.25 0 0018 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"/>
58
+ </svg>
59
+ </button>
60
+ </div>
61
+
62
+ <div
63
+ x-show="open"
64
+ x-cloak
65
+ x-transition.opacity.duration.100ms
66
+ class="shamar-m2o__dropdown absolute left-0 right-0 top-full z-50 mt-1 max-h-72 overflow-auto rounded-md border border-default bg-neutral-primary text-sm shadow-lg"
67
+ role="listbox"
68
+ >
69
+ <template x-if="loading">
70
+ <div class="px-3 py-2 text-xs text-body-subtle">Searching…</div>
71
+ </template>
72
+ <template x-if="!loading && results.length === 0 && !createCandidate">
73
+ <div class="px-3 py-2 text-xs text-body-subtle">No matches.</div>
74
+ </template>
75
+ <ul x-show="!loading && (results.length > 0 || createCandidate || canCreateAndEdit)">
76
+ <template x-for="(item, i) in results" :key="item.id">
77
+ <li
78
+ role="option"
79
+ @mousedown.prevent="pick(item)"
80
+ @mousemove="cursor = i"
81
+ :class="i === cursor ? 'bg-brand-softer text-fg-brand' : 'text-body hover:bg-neutral-secondary'"
82
+ class="flex cursor-pointer items-center justify-between px-3 py-1.5"
83
+ >
84
+ <span x-text="item.label" class="truncate"></span>
85
+ <span x-show="String(item.id) === String(value)" class="text-2xs font-medium text-fg-brand">selected</span>
86
+ </li>
87
+ </template>
88
+ <template x-if="createCandidate">
89
+ <li
90
+ role="option"
91
+ @mousedown.prevent="createFromQuery()"
92
+ @mousemove="cursor = results.length"
93
+ :class="cursor === results.length ? 'bg-success-soft text-success-strong' : 'text-fg-success hover:bg-neutral-secondary'"
94
+ class="flex cursor-pointer items-center gap-1.5 border-t border-default px-3 py-1.5"
95
+ >
96
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
97
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
98
+ </svg>
99
+ <span>Create "<span x-text="query"></span>"</span>
100
+ </li>
101
+ </template>
102
+ <template x-if="canCreateAndEdit">
103
+ <li
104
+ role="option"
105
+ @mousedown.prevent="createAndEdit()"
106
+ class="flex cursor-pointer items-center gap-1.5 border-t border-default px-3 py-1.5 text-body hover:bg-neutral-secondary"
107
+ >
108
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
109
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L10.582 16.07a4.5 4.5 0 01-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 011.13-1.897l8.932-8.931z"/>
110
+ </svg>
111
+ <span>Create and edit…</span>
112
+ </li>
113
+ </template>
114
+ </ul>
115
+ </div>
116
+ </div>
@@ -0,0 +1,139 @@
1
+ {{-- Relation table (HasMany / M2M). Expects `relationConfig`. --}}
2
+ @if(relationConfig.requiresParent)
3
+ <div class="rounded-md border border-dashed border-default px-4 py-8 text-center text-sm text-body-subtle">
4
+ Save this record first to manage related {{ relationConfig.singularLabel }} records.
5
+ </div>
6
+ @else
7
+ <div
8
+ class="shamar-m2m-table relative w-full"
9
+ data-shamar-m2m-config="{{ jsonAttr(relationConfig) }}"
10
+ x-data="shamarM2mTableFromEl($el)"
11
+ @click.outside="closeAdd()"
12
+ @keydown.escape.prevent.stop="closeAdd()"
13
+ >
14
+ @if(relationConfig.kind !== 'hasMany')
15
+ <template x-for="item in selected" :key="'h-' + item.id">
16
+ <input type="hidden" name="{{ relationConfig.name }}[]" :value="item.id" />
17
+ </template>
18
+ @endif
19
+
20
+ <div class="shamar-m2m-table__card rounded-md border border-default">
21
+ <div class="shamar-m2m-table__toolbar flex flex-wrap items-center justify-between gap-2 border-b border-default bg-surface-header px-3 py-2">
22
+ <p class="text-xs font-medium text-body-subtle">
23
+ <span x-text="selected.length"></span> linked
24
+ <span x-text="singularLabel.toLowerCase()"></span><span x-show="selected.length !== 1">s</span>
25
+ </p>
26
+ <div class="shamar-m2m-table__add relative" x-show="!readonly">
27
+ <button
28
+ type="button"
29
+ class="inline-flex items-center gap-1 rounded-md border border-default bg-neutral-primary px-2.5 py-1 text-xs font-medium text-body hover:border-fg-brand hover:text-fg-brand"
30
+ @click="toggleAdd()"
31
+ >
32
+ <svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
33
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
34
+ </svg>
35
+ Add
36
+ </button>
37
+ <div
38
+ x-show="addOpen"
39
+ x-cloak
40
+ x-transition.opacity.duration.100ms
41
+ class="shamar-m2m-table__add-dropdown absolute right-0 z-50 mt-1 w-72 rounded-md border border-default bg-neutral-primary text-sm shadow-lg"
42
+ >
43
+ <div class="border-b border-default p-2">
44
+ <input
45
+ type="text"
46
+ x-ref="addInput"
47
+ x-model="query"
48
+ @input.debounce.250ms="fetchResults()"
49
+ @keydown.arrow-down.prevent="moveCursor(1)"
50
+ @keydown.arrow-up.prevent="moveCursor(-1)"
51
+ @keydown.enter.prevent="onEnter()"
52
+ placeholder="Search to add…"
53
+ autocomplete="off"
54
+ class="shamar-input w-full text-sm"
55
+ />
56
+ </div>
57
+ <div class="max-h-56 overflow-auto">
58
+ <template x-if="loading">
59
+ <div class="px-3 py-2 text-xs text-body-subtle">Searching…</div>
60
+ </template>
61
+ <template x-if="!loading && availableResults.length === 0 && !createCandidate">
62
+ <div class="px-3 py-2 text-xs text-body-subtle">No matches.</div>
63
+ </template>
64
+ <ul x-show="!loading && (availableResults.length > 0 || createCandidate || canCreateAndEdit)">
65
+ <template x-for="(item, i) in availableResults" :key="item.id">
66
+ <li
67
+ class="cursor-pointer truncate px-3 py-1.5"
68
+ :class="i === cursor ? 'bg-brand-softer text-fg-brand' : 'text-body hover:bg-neutral-secondary'"
69
+ @mousedown.prevent="pick(item)"
70
+ @mousemove="cursor = i"
71
+ x-text="item.label"
72
+ ></li>
73
+ </template>
74
+ <template x-if="createCandidate">
75
+ <li
76
+ class="flex cursor-pointer items-center gap-1.5 border-t border-default px-3 py-1.5 text-fg-success hover:bg-neutral-secondary"
77
+ @click="createFromQuery()"
78
+ >
79
+ <span>Create "<span x-text="query"></span>"</span>
80
+ </li>
81
+ </template>
82
+ <template x-if="canCreateAndEdit">
83
+ <li
84
+ class="flex cursor-pointer items-center gap-1.5 border-t border-default px-3 py-1.5 text-body hover:bg-neutral-secondary"
85
+ @click="createAndEdit()"
86
+ >
87
+ Create and edit…
88
+ </li>
89
+ </template>
90
+ </ul>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <table class="w-full text-sm">
97
+ <thead>
98
+ <tr class="border-b border-default text-left text-xs uppercase tracking-wider text-body-subtle">
99
+ <th class="px-3 py-2 font-semibold">Record</th>
100
+ <th class="px-3 py-2 text-right font-semibold" x-show="!readonly">Actions</th>
101
+ </tr>
102
+ </thead>
103
+ <tbody>
104
+ <template x-if="selected.length === 0">
105
+ <tr>
106
+ <td colspan="2" class="px-3 py-8 text-center text-xs text-body-subtle">No linked records yet.</td>
107
+ </tr>
108
+ </template>
109
+ <template x-for="item in selected" :key="item.id">
110
+ <tr class="border-b border-default last:border-0 hover:bg-surface-row-hover">
111
+ <td class="px-3 py-2">
112
+ <button
113
+ type="button"
114
+ class="text-left text-heading hover:text-fg-brand hover:underline"
115
+ @click="openRecord(item)"
116
+ x-text="item.label"
117
+ ></button>
118
+ </td>
119
+ <td class="px-3 py-2 text-right" x-show="!readonly">
120
+ <div class="inline-flex items-center gap-1 text-xs">
121
+ <button
122
+ type="button"
123
+ class="rounded-md px-2 py-1 text-body hover:bg-surface-hover"
124
+ @click="openRecord(item)"
125
+ >Open</button>
126
+ <button
127
+ type="button"
128
+ class="rounded-md px-2 py-1 text-fg-danger hover:bg-danger-soft"
129
+ @click="remove(item.id)"
130
+ >Unlink</button>
131
+ </div>
132
+ </td>
133
+ </tr>
134
+ </template>
135
+ </tbody>
136
+ </table>
137
+ </div>
138
+ </div>
139
+ @endif
@@ -0,0 +1,82 @@
1
+ {{--
2
+ Renders one form container: Section card, Fieldset legend, or plain grid.
3
+ Expects: section, mode
4
+ --}}
5
+ @if((section.kind ?? 'section') === 'plain')
6
+ <div class="shamar-form-plain">
7
+ <div class="shamar-form-grid" style="--shamar-form-cols: {{ section.columns ?? 2 }}">
8
+ @each(field in section.fields)
9
+ @if(!field.hiddenOnForm && !(field.createOnly && mode === 'edit'))
10
+ <div
11
+ class="shamar-field-cell"
12
+ style="{{ resolveGridItemStyle(field, section.columns ?? 2) }}"
13
+ x-show="isVisible('{{ field.name }}')"
14
+ >
15
+ @include('shamar::partials/field-input')
16
+ </div>
17
+ @endif
18
+ @endeach
19
+ </div>
20
+ </div>
21
+ @elseif((section.kind ?? 'section') === 'fieldset')
22
+ <fieldset class="shamar-form-fieldset{{ section.card ? ' shamar-form-fieldset--card' : '' }}">
23
+ @if(section.title)
24
+ <legend class="shamar-form-fieldset__legend">{{ section.title }}</legend>
25
+ @endif
26
+ @if(section.description)
27
+ <p class="shamar-form-fieldset__description">{{ section.description }}</p>
28
+ @endif
29
+ <div class="shamar-form-grid" style="--shamar-form-cols: {{ section.columns ?? 2 }}">
30
+ @each(field in section.fields)
31
+ @if(!field.hiddenOnForm && !(field.createOnly && mode === 'edit'))
32
+ <div
33
+ class="shamar-field-cell"
34
+ style="{{ resolveGridItemStyle(field, section.columns ?? 2) }}"
35
+ x-show="isVisible('{{ field.name }}')"
36
+ >
37
+ @include('shamar::partials/field-input')
38
+ </div>
39
+ @endif
40
+ @endeach
41
+ </div>
42
+ </fieldset>
43
+ @else
44
+ <section class="shamar-form-section rounded-xl overflow-hidden{{ section.card !== false ? ' shamar-card' : ' shamar-form-section--plain' }}">
45
+ @if(section.title || section.icon || section.description)
46
+ <header class="shamar-form-section__header{{ section.description ? '' : ' shamar-form-section__header--centered' }}">
47
+ @if(section.icon)
48
+ <span class="shamar-form-section__icon" aria-hidden="true" data-icon="{{ section.icon }}">
49
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" class="w-5 h-5">
50
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
51
+ </svg>
52
+ </span>
53
+ @endif
54
+ @if(section.title || section.description)
55
+ <div class="shamar-form-section__titles min-w-0">
56
+ @if(section.title)
57
+ <h3 class="shamar-form-section__title">{{ section.title }}</h3>
58
+ @endif
59
+ @if(section.description)
60
+ <p class="shamar-form-section__description">{{ section.description }}</p>
61
+ @endif
62
+ </div>
63
+ @endif
64
+ </header>
65
+ @endif
66
+ <div class="shamar-form-section__body">
67
+ <div class="shamar-form-grid" style="--shamar-form-cols: {{ section.columns ?? 2 }}">
68
+ @each(field in section.fields)
69
+ @if(!field.hiddenOnForm && !(field.createOnly && mode === 'edit'))
70
+ <div
71
+ class="shamar-field-cell"
72
+ style="{{ resolveGridItemStyle(field, section.columns ?? 2) }}"
73
+ x-show="isVisible('{{ field.name }}')"
74
+ >
75
+ @include('shamar::partials/field-input')
76
+ </div>
77
+ @endif
78
+ @endeach
79
+ </div>
80
+ </div>
81
+ </section>
82
+ @endif
@@ -0,0 +1,14 @@
1
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.8" aria-hidden="true">
2
+ @if((root && root.icon === 'home') || (item && (item.icon ?? 'squares-2x2') === 'home'))
3
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/>
4
+ @elseif((root && root.icon === 'cog') || (item && (item.icon ?? 'squares-2x2') === 'cog'))
5
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 010 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 010-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28z"/>
6
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
7
+ @elseif((root && root.icon === 'building') || (item && (item.icon ?? 'squares-2x2') === 'building'))
8
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 21h16.5M4.5 3h15M5.25 3v18m13.5-18v18M9 6.75h1.5m-1.5 3h1.5m-1.5 3h1.5m3-6H15m-1.5 3H15m-1.5 3H15M9 21v-3.375c0-.621.504-1.125 1.125-1.125h3.75c.621 0 1.125.504 1.125 1.125V21"/>
9
+ @elseif((root && root.icon === 'users') || (item && (item.icon ?? 'squares-2x2') === 'users'))
10
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766C4.09 17.49 6 15.5 6 13.5V11.25m9 7.878V11.25M12 7.875a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0z"/>
11
+ @else
12
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z"/>
13
+ @endif
14
+ </svg>
@@ -0,0 +1,73 @@
1
+ <tr class="hover:bg-surface-row-hover transition-colors" data-shamar-record-id="{{ record.id }}">
2
+ <td class="px-3 py-3">
3
+ @if(bulkActions && bulkActions.length)
4
+ <input
5
+ type="checkbox"
6
+ class="rounded border-default"
7
+ :checked="selected.includes({{ jsonAttr(String(record.id)) }})"
8
+ @change="toggleId({{ jsonAttr(String(record.id)) }})"
9
+ aria-label="Select record"
10
+ />
11
+ @endif
12
+ </td>
13
+ @each(column in resource.columns)
14
+ <td class="px-4 py-3 text-body {{ resolveAlignmentClass(column) }}">
15
+ @if(column.type === 'boolean' || column.format === 'boolean' || column.format === 'toggle')
16
+ @!component('shamar::partials/boolean-value', {
17
+ checked: !!record[column.name],
18
+ label: cellValue(record, column),
19
+ })
20
+ @elseif(column.format === 'badge' || column.type === 'tags')
21
+ @let(badges = badgeValues(record, column))
22
+ @if(badges.length)
23
+ <div class="shamar-badges">
24
+ @each(badge in badges)
25
+ <span class="shamar-badge">{{ badge }}</span>
26
+ @endeach
27
+ </div>
28
+ @else
29
+
30
+ @endif
31
+ @else
32
+ <a href="{{ basePath }}/{{ resource.slug }}/{{ record.id }}{{ recordNavQuery(query) }}" class="text-heading hover:text-fg-brand hover:underline">{{ cellValue(record, column) }}</a>
33
+ @endif
34
+ </td>
35
+ @endeach
36
+ <td class="px-4 py-3 text-right">
37
+ <div class="inline-flex flex-wrap items-center justify-end gap-1 text-xs">
38
+ @if(rowActions)
39
+ @each(action in rowActions)
40
+ @if(action.name === 'view')
41
+ <a href="{{ basePath }}/{{ resource.slug }}/{{ record.id }}{{ recordNavQuery(query) }}" class="px-2 py-1 rounded-md text-body hover:bg-surface-hover">{{ action.label }}</a>
42
+ @elseif(action.name === 'edit' && showEditButton)
43
+ <a href="{{ basePath }}/{{ resource.slug }}/{{ record.id }}/edit{{ recordNavQuery(query) }}" class="px-2 py-1 rounded-md text-body hover:bg-surface-hover">{{ action.label }}</a>
44
+ @elseif(action.name === 'delete' && showDeleteButton)
45
+ <button
46
+ type="button"
47
+ class="px-2 py-1 rounded-md text-fg-danger hover:bg-danger-soft"
48
+ @click="$dispatch('shamar-open-confirm', { variant: 'danger', mode: 'confirm', title: {{ jsonAttr(action.label) }}, message: {{ jsonAttr(action.confirm || 'Are you sure?') }}, confirmLabel: 'Delete', action: '{{ basePath }}/{{ resource.slug }}/{{ record.id }}/delete' })"
49
+ >{{ action.label }}</button>
50
+ @elseif(action.name === 'revoke' && !record.revokedAt)
51
+ <button
52
+ type="button"
53
+ class="px-2 py-1 rounded-md text-fg-danger hover:bg-danger-soft"
54
+ @click="$dispatch('shamar-open-confirm', { variant: 'danger', mode: 'confirm', title: {{ jsonAttr(action.label) }}, message: {{ jsonAttr(action.confirm || 'Are you sure?') }}, confirmLabel: {{ jsonAttr(action.label) }}, action: '{{ basePath }}/{{ resource.slug }}/{{ record.id }}/action/{{ action.name }}' })"
55
+ >{{ action.label }}</button>
56
+ @elseif(action.name === 'reactivate' && record.revokedAt)
57
+ <button
58
+ type="button"
59
+ class="px-2 py-1 rounded-md text-body hover:bg-surface-hover"
60
+ @click="$dispatch('shamar-open-confirm', { variant: 'primary', mode: 'confirm', title: {{ jsonAttr(action.label) }}, message: {{ jsonAttr(action.confirm || 'Are you sure?') }}, confirmLabel: {{ jsonAttr(action.label) }}, action: '{{ basePath }}/{{ resource.slug }}/{{ record.id }}/action/{{ action.name }}' })"
61
+ >{{ action.label }}</button>
62
+ @elseif(action.name !== 'view' && action.name !== 'edit' && action.name !== 'delete' && action.name !== 'revoke' && action.name !== 'reactivate')
63
+ <button
64
+ type="button"
65
+ class="px-2 py-1 rounded-md text-body hover:bg-surface-hover {{ action.color === 'danger' ? 'text-fg-danger hover:bg-danger-soft' : '' }}"
66
+ @click="$dispatch('shamar-open-confirm', { variant: {{ jsonAttr(action.color === 'danger' ? 'danger' : 'primary') }}, mode: 'confirm', title: {{ jsonAttr(action.label) }}, message: {{ jsonAttr(action.confirm || 'Are you sure?') }}, confirmLabel: {{ jsonAttr(action.label) }}, action: '{{ basePath }}/{{ resource.slug }}/{{ record.id }}/action/{{ action.name }}' })"
67
+ >{{ action.label }}</button>
68
+ @endif
69
+ @endeach
70
+ @endif
71
+ </div>
72
+ </td>
73
+ </tr>