@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,269 @@
1
+ <div
2
+ class="px-4 py-3 space-y-3"
3
+ x-data='shamarListToolbar({
4
+ search: {{ jsonAttr(query.search) }},
5
+ filters: {{ jsonAttr(query.filters) }},
6
+ filtersLockedEmpty: {{ jsonAttr(filtersLockedEmpty) }},
7
+ groupBy: {{ jsonAttr(query.groupBy) }},
8
+ groupLockedEmpty: {{ jsonAttr(groupLockedEmpty) }},
9
+ sort: {{ jsonAttr(query.sort) }},
10
+ direction: {{ jsonAttr(query.direction) }},
11
+ perPage: {{ jsonAttr(query.perPage) }},
12
+ trashed: {{ jsonAttr(query.trashed) }},
13
+ headers: {{ jsonAttr(listHeaders) }},
14
+ basePath: {{ jsonAttr(basePath) }},
15
+ slug: {{ jsonAttr(resource.slug) }},
16
+ view: {{ jsonAttr('table') }},
17
+ allPerPage: {{ jsonAttr(listAllPerPage) }}
18
+ })'
19
+ >
20
+ <div class="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
21
+ <div
22
+ class="relative flex-1 min-w-0 bg-neutral-primary border border-default rounded-lg transition-shadow focus-within:ring-2 focus-within:ring-fg-brand focus-within:border-fg-brand"
23
+ >
24
+ <div class="flex items-center flex-wrap gap-1.5 px-2.5 py-1.5 min-h-[40px]">
25
+ <svg class="w-4 h-4 text-body-subtle shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
26
+ <path stroke-linecap="round" stroke-linejoin="round" d="M21 21l-5.197-5.197m0 0A7.5 7.5 0 105.196 15.803 7.5 7.5 0 0015.803 15.803z"/>
27
+ </svg>
28
+
29
+ <template x-for="(chip, i) in chips" :key="'c-' + i + '-' + chip.field">
30
+ <span class="shamar-chip">
31
+ <span x-text="chip.label || (chip.field + ': ' + chip.value)" class="max-w-[200px] truncate"></span>
32
+ <button type="button" @click="removeChip(i)" aria-label="Remove filter" class="shamar-chip__remove">
33
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
34
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
35
+ </svg>
36
+ </button>
37
+ </span>
38
+ </template>
39
+
40
+ <template x-if="groupBy">
41
+ <span class="shamar-chip shamar-chip--group">
42
+ <span>Grouped by <span class="font-semibold" x-text="groupByLabel()"></span></span>
43
+ <button type="button" @click="clearGroupBy()" aria-label="Clear grouping" class="shamar-chip__remove">
44
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="3">
45
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
46
+ </svg>
47
+ </button>
48
+ </span>
49
+ </template>
50
+
51
+ <input
52
+ type="search"
53
+ x-ref="listSearchInput"
54
+ x-model="searchInput"
55
+ @input.debounce.350ms="onSearchChange()"
56
+ @keydown.escape="clearSearch()"
57
+ placeholder="Search…"
58
+ class="flex-1 min-w-[120px] bg-transparent border-0 px-1 py-1 text-sm text-body placeholder:text-body-subtle focus:outline-none focus:ring-0"
59
+ />
60
+
61
+ <button
62
+ x-show="chips.length || groupBy || searchInput"
63
+ x-cloak
64
+ type="button"
65
+ @click="clearAll()"
66
+ class="text-[10px] text-body-subtle hover:text-fg-brand underline shrink-0 px-1"
67
+ >Clear all</button>
68
+ </div>
69
+ </div>
70
+
71
+ <div class="flex items-center gap-2 shrink-0 flex-wrap justify-end">
72
+ <div
73
+ class="shamar-toolbar-menu"
74
+ x-show="filterableHeaders().length"
75
+ x-cloak
76
+ @click.outside="panelOpen = false; openFilterField = null"
77
+ @keydown.escape.window="if (panelOpen) { panelOpen = false; openFilterField = null }"
78
+ >
79
+ <button
80
+ type="button"
81
+ class="shamar-toolbar-btn"
82
+ :class="{ 'is-active': panelOpen || chips.length }"
83
+ :aria-expanded="panelOpen.toString()"
84
+ aria-haspopup="menu"
85
+ @click="panelOpen = !panelOpen; if (!panelOpen) openFilterField = null"
86
+ >
87
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.75" aria-hidden="true">
88
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 01-.659 1.591l-5.432 5.432a2.25 2.25 0 00-.659 1.591v2.927a2.25 2.25 0 01-1.244 2.013L9.75 21v-6.568a2.25 2.25 0 00-.659-1.591L3.659 7.409A2.25 2.25 0 013 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0112 3z"/>
89
+ </svg>
90
+ Filters
91
+ <span x-show="chips.length" x-cloak class="shamar-toolbar-btn__badge" x-text="chips.length"></span>
92
+ </button>
93
+
94
+ <div
95
+ x-show="panelOpen"
96
+ x-cloak
97
+ x-transition.opacity.duration.120ms
98
+ class="shamar-toolbar-panel"
99
+ role="menu"
100
+ aria-label="Filters"
101
+ >
102
+ <div class="shamar-toolbar-panel__head">
103
+ <p class="text-xs font-semibold uppercase tracking-wide text-body-subtle">Filter by</p>
104
+ <button
105
+ type="button"
106
+ class="text-xs text-body-subtle underline hover:text-fg-brand"
107
+ x-show="chips.length"
108
+ @click="chips = []; filtersLockedEmpty = true; applyFilters()"
109
+ >Clear filters</button>
110
+ </div>
111
+
112
+ <template x-if="!openFilterField">
113
+ <div class="shamar-toolbar-panel__list">
114
+ <template x-for="h in filterableHeaders()" :key="'f-' + h.name">
115
+ <button
116
+ type="button"
117
+ class="shamar-toolbar-panel__item"
118
+ role="menuitem"
119
+ @click="toggleFilterField(h.name); if (h.filter_kind === 'm2o') searchM2o(h)"
120
+ >
121
+ <span x-text="h.label"></span>
122
+ <svg class="w-3.5 h-3.5 text-body-subtle" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/></svg>
123
+ </button>
124
+ </template>
125
+ </div>
126
+ </template>
127
+
128
+ <template x-if="openFilterField">
129
+ <div>
130
+ <button
131
+ type="button"
132
+ class="shamar-toolbar-panel__back"
133
+ @click="openFilterField = null"
134
+ >
135
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/></svg>
136
+ <span x-text="(headers.find(h => h.name === openFilterField) || {}).label || 'Back'"></span>
137
+ </button>
138
+
139
+ <template x-for="h in filterableHeaders().filter(item => item.name === openFilterField)" :key="'fv-' + h.name">
140
+ <div class="shamar-toolbar-panel__list">
141
+ <template x-if="h.filter_kind === 'boolean'">
142
+ <div>
143
+ <button type="button" class="shamar-toolbar-panel__item" @click="addBooleanFilter(h, true)">Yes</button>
144
+ <button type="button" class="shamar-toolbar-panel__item" @click="addBooleanFilter(h, false)">No</button>
145
+ </div>
146
+ </template>
147
+
148
+ <template x-if="h.filter_kind === 'select'">
149
+ <div>
150
+ <template x-for="opt in (h.options || [])" :key="'so-' + opt.value">
151
+ <button type="button" class="shamar-toolbar-panel__item" @click="addSelectFilter(h, opt)" x-text="opt.label"></button>
152
+ </template>
153
+ </div>
154
+ </template>
155
+
156
+ <template x-if="h.filter_kind === 'm2o'">
157
+ <div class="p-2 space-y-2">
158
+ <input
159
+ type="search"
160
+ class="shamar-select w-full"
161
+ placeholder="Search…"
162
+ x-model="m2oQuery[h.name]"
163
+ @input.debounce.300ms="searchM2o(h)"
164
+ />
165
+ <div class="max-h-48 overflow-auto rounded-md border border-default">
166
+ <template x-for="opt in (m2oResults[h.name] || [])" :key="'m2o-' + opt.id">
167
+ <button
168
+ type="button"
169
+ class="shamar-toolbar-panel__item"
170
+ @click="addM2oFilter(h, opt)"
171
+ x-text="opt.label"
172
+ ></button>
173
+ </template>
174
+ <p
175
+ x-show="!(m2oResults[h.name] || []).length"
176
+ class="px-3 py-2 text-xs text-body-subtle"
177
+ >No matches</p>
178
+ </div>
179
+ </div>
180
+ </template>
181
+ </div>
182
+ </template>
183
+ </div>
184
+ </template>
185
+ </div>
186
+ </div>
187
+
188
+ <div
189
+ class="shamar-toolbar-menu"
190
+ x-show="groupableHeaders().length"
191
+ x-cloak
192
+ @click.outside="groupOpen = false"
193
+ @keydown.escape.window="if (groupOpen) groupOpen = false"
194
+ >
195
+ <button
196
+ type="button"
197
+ class="shamar-toolbar-btn"
198
+ :class="{ 'is-active': groupOpen || groupBy }"
199
+ :aria-expanded="groupOpen.toString()"
200
+ aria-haspopup="menu"
201
+ @click="groupOpen = !groupOpen"
202
+ >
203
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.75" aria-hidden="true">
204
+ <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"/>
205
+ </svg>
206
+ Group
207
+ </button>
208
+
209
+ <div
210
+ x-show="groupOpen"
211
+ x-cloak
212
+ x-transition.opacity.duration.120ms
213
+ class="shamar-toolbar-panel"
214
+ role="menu"
215
+ aria-label="Group by"
216
+ >
217
+ <div class="shamar-toolbar-panel__head">
218
+ <p class="text-xs font-semibold uppercase tracking-wide text-body-subtle">Group by</p>
219
+ <button
220
+ type="button"
221
+ class="text-xs text-body-subtle underline hover:text-fg-brand"
222
+ x-show="groupBy"
223
+ @click="clearGroupBy()"
224
+ >Clear</button>
225
+ </div>
226
+ <div class="shamar-toolbar-panel__list">
227
+ <template x-for="h in groupableHeaders()" :key="'g-' + h.name">
228
+ <button
229
+ type="button"
230
+ class="shamar-toolbar-panel__item"
231
+ :class="{ 'is-selected': groupBy === h.name }"
232
+ role="menuitem"
233
+ @click="setGroupBy(h.name)"
234
+ >
235
+ <span x-text="h.label"></span>
236
+ <svg x-show="groupBy === h.name" class="w-3.5 h-3.5 text-fg-brand" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"/></svg>
237
+ </button>
238
+ </template>
239
+ </div>
240
+ </div>
241
+ </div>
242
+
243
+ <label for="list-per-page" class="text-xs font-medium text-body-subtle whitespace-nowrap">Rows</label>
244
+ <select
245
+ id="list-per-page"
246
+ x-model="perPage"
247
+ @change="reload()"
248
+ class="shamar-select w-auto min-w-[5.5rem]"
249
+ >
250
+ <option value="10">10</option>
251
+ <option value="15">15</option>
252
+ <option value="20">20</option>
253
+ <option value="50">50</option>
254
+ <option value="all">All records</option>
255
+ </select>
256
+ <button
257
+ type="button"
258
+ class="shamar-list-refresh"
259
+ data-shamar-list-refresh
260
+ title="Refresh (double-click for auto-refresh every 10s)"
261
+ aria-label="Refresh list"
262
+ >
263
+ <svg class="shamar-list-refresh__icon h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" aria-hidden="true">
264
+ <path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182"/>
265
+ </svg>
266
+ </button>
267
+ </div>
268
+ </div>
269
+ </div>
@@ -0,0 +1,21 @@
1
+ <nav class="flex-1 px-2 py-3 overflow-y-auto space-y-4" aria-label="Navigation">
2
+ @each(group in navGroups)
3
+ <div>
4
+ <p class="px-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-body-subtle">{{ group.name }}</p>
5
+ <div class="space-y-0.5">
6
+ @each(item in group.items)
7
+ <a
8
+ href="{{ basePath }}/{{ item.slug }}"
9
+ class="nav-item flex items-center gap-2.5 px-3 py-2 rounded-md text-sm {{ resource && item.slug === resource.slug ? 'nav-active' : '' }}"
10
+ @click="sidebarOpen = false"
11
+ >
12
+ <span class="shrink-0 w-4 h-4">
13
+ @include('shamar::partials/icon')
14
+ </span>
15
+ <span class="truncate">{{ item.label }}</span>
16
+ </a>
17
+ @endeach
18
+ </div>
19
+ </div>
20
+ @endeach
21
+ </nav>
@@ -0,0 +1,58 @@
1
+ <aside
2
+ class="fixed md:relative inset-y-0 left-0 z-30 w-64 min-h-screen bg-shamar-shell flex flex-col flex-shrink-0 md:translate-x-0 transition-transform duration-200 ease-in-out"
3
+ :class="{ '-translate-x-full': !sidebarOpen }"
4
+ >
5
+ <div class="relative px-4 shrink-0 flex items-center min-h-[60px]">
6
+ <a href="{{ basePath }}" class="flex min-w-0 items-center gap-2.5">
7
+ @if(branding.logoUrl)
8
+ <img
9
+ src="{{ branding.logoUrl }}"
10
+ alt="{{ branding.brandName }}"
11
+ class="h-8 w-auto max-w-[9rem] object-contain dark:hidden"
12
+ />
13
+ <img
14
+ src="{{ branding.logoDarkUrl ?? branding.logoUrl }}"
15
+ alt="{{ branding.brandName }}"
16
+ class="hidden h-8 w-auto max-w-[9rem] object-contain dark:block"
17
+ />
18
+ @endif
19
+ <span class="truncate text-base font-semibold text-heading">{{ panelTitle }}</span>
20
+ </a>
21
+ <button
22
+ type="button"
23
+ @click="sidebarOpen = false"
24
+ aria-label="Close sidebar"
25
+ class="absolute right-3 top-1/2 -translate-y-1/2 md:hidden p-1 rounded-md text-body-subtle hover:text-heading hover:bg-surface-hover"
26
+ >
27
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
28
+ <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/>
29
+ </svg>
30
+ </button>
31
+ </div>
32
+
33
+ <nav class="hidden md:block flex-1 px-2 py-3 space-y-0.5 overflow-y-auto" aria-label="Applications">
34
+ @each(root in menuRoots)
35
+ <a
36
+ href="{{ root.href }}"
37
+ class="nav-item flex items-center gap-2.5 px-3 py-2 rounded-md text-sm {{ root.active ? 'nav-active' : '' }}"
38
+ @click="sidebarOpen = false"
39
+ >
40
+ <span class="shrink-0 w-4 h-4">
41
+ @include('shamar::partials/icon')
42
+ </span>
43
+ <span class="truncate">{{ root.label }}</span>
44
+ </a>
45
+ @endeach
46
+ </nav>
47
+
48
+ <div class="md:hidden flex-1 min-h-0 flex flex-col overflow-hidden">
49
+ @include('shamar::partials/nav-mobile')
50
+ </div>
51
+
52
+ <div class="mt-auto px-4 py-3 text-xs text-body-subtle hidden md:block">
53
+ @if(branding.copyrightText)
54
+ <p class="mb-2 leading-relaxed">{{ branding.copyrightText }}</p>
55
+ @endif
56
+ <p class="truncate">Signed in as <span class="font-semibold text-heading">{{ user.name }}</span></p>
57
+ </div>
58
+ </aside>
@@ -0,0 +1,130 @@
1
+ <div class="pt-1 pb-2">
2
+ @if(breadcrumbs && breadcrumbs.length)
3
+ <nav aria-label="Breadcrumb" class="mb-2 flex items-center gap-1.5 text-xs text-body-subtle flex-wrap">
4
+ @each(crumb in breadcrumbs)
5
+ @if(!$last)
6
+ @if(crumb.href)
7
+ <a href="{{ crumb.href }}" class="hover:text-fg-brand hover:underline transition-colors truncate max-w-[140px]">{{ crumb.label }}</a>
8
+ @else
9
+ <span class="truncate max-w-[140px]">{{ crumb.label }}</span>
10
+ @endif
11
+ <svg class="w-3 h-3 flex-shrink-0 text-body-subtle/50" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
12
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
13
+ </svg>
14
+ @else
15
+ @if(crumb.href)
16
+ <a href="{{ crumb.href }}" class="text-body hover:text-fg-brand hover:underline transition-colors truncate max-w-[200px]">{{ crumb.label }}</a>
17
+ @else
18
+ <span class="text-body truncate max-w-[200px]">{{ crumb.label }}</span>
19
+ @endif
20
+ @endif
21
+ @endeach
22
+ </nav>
23
+ @endif
24
+
25
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
26
+ <div class="min-w-0 flex items-center gap-3 flex-wrap">
27
+ <div class="min-w-0">
28
+ <h1 class="text-xl font-semibold text-heading truncate">{{ pageTitle }}</h1>
29
+ @if(pageSubtitle)
30
+ <p class="mt-1 text-sm text-body-subtle">{{ pageSubtitle }}</p>
31
+ @endif
32
+ </div>
33
+ @if(mode === 'edit' || mode === 'create' || mode === 'show')
34
+ <span class="inline-flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-wider px-2 py-0.5 rounded-full
35
+ {{ mode === 'edit' ? 'bg-warning-soft text-warning-strong' : mode === 'create' ? 'bg-success-soft text-success-strong' : 'bg-badge-neutral text-body-subtle' }}">
36
+ {{ mode === 'show' ? 'detail' : mode }}
37
+ </span>
38
+ @endif
39
+ </div>
40
+
41
+ <div class="flex flex-wrap items-center gap-2 shrink-0">
42
+ @if(showCreateButton)
43
+ <a href="{{ basePath }}/{{ resource.slug }}/create" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white rounded-md bg-fg-brand hover:opacity-90 transition-opacity">
44
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
45
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15"/>
46
+ </svg>
47
+ New
48
+ </a>
49
+ @endif
50
+
51
+ @if(mode === 'edit' || mode === 'create')
52
+ <button
53
+ type="button"
54
+ data-shamar-form-save
55
+ title="Save (Ctrl+S)"
56
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white rounded-md bg-fg-brand hover:opacity-90 transition"
57
+ >
58
+ {{ mode === 'create' ? 'Create' : 'Save' }}
59
+ </button>
60
+ @if(mode === 'edit')
61
+ <a
62
+ href="{{ basePath }}/{{ resource.slug }}/{{ record.id }}{{ recordPager ? recordPager.navQuery : '' }}"
63
+ title="View details"
64
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-body rounded-md hover:bg-surface-hover transition"
65
+ >
66
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.75" aria-hidden="true">
67
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
68
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
69
+ </svg>
70
+ View
71
+ </a>
72
+ <a
73
+ href="{{ basePath }}/{{ resource.slug }}"
74
+ data-shamar-list-href="{{ resource.slug }}"
75
+ data-shamar-skip-autosave
76
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-body rounded-md hover:bg-surface-hover transition"
77
+ >Cancel</a>
78
+ @if(showDeleteButton)
79
+ <button
80
+ type="button"
81
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-fg-danger rounded-md hover:bg-danger-soft transition"
82
+ @click="$dispatch('shamar-open-confirm', { variant: 'danger', mode: 'confirm', title: 'Delete {{ resource.singularLabel }}?', message: 'This action cannot be undone.', confirmLabel: 'Delete', action: '{{ basePath }}/{{ resource.slug }}/{{ record.id }}/delete' })"
83
+ >Delete</button>
84
+ @endif
85
+ @else
86
+ <a
87
+ href="{{ basePath }}/{{ resource.slug }}"
88
+ data-shamar-list-href="{{ resource.slug }}"
89
+ data-shamar-skip-autosave
90
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-body rounded-md hover:bg-surface-hover transition"
91
+ >Cancel</a>
92
+ @endif
93
+ @elseif(mode === 'show')
94
+ @if(showEditButton !== false && showEditButton)
95
+ <a href="{{ basePath }}/{{ resource.slug }}/{{ record.id }}/edit{{ recordPager ? recordPager.navQuery : '' }}" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white rounded-md bg-fg-brand hover:opacity-90 transition">Edit</a>
96
+ @endif
97
+ @if(recordActions)
98
+ @each(action in recordActions)
99
+ @if(action.name !== 'view' && action.name !== 'edit' && action.name !== 'delete')
100
+ <button
101
+ type="button"
102
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default rounded-md transition
103
+ {{ action.color === 'danger' ? 'text-fg-danger hover:bg-danger-soft' : 'text-body hover:bg-surface-hover' }}"
104
+ @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 }}' })"
105
+ >{{ action.label }}</button>
106
+ @endif
107
+ @endeach
108
+ @endif
109
+ <a href="{{ basePath }}/{{ resource.slug }}" data-shamar-list-href="{{ resource.slug }}" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-body rounded-md hover:bg-surface-hover transition">Back to list</a>
110
+ @if(showDeleteButton)
111
+ <button
112
+ type="button"
113
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-fg-danger rounded-md hover:bg-danger-soft transition"
114
+ @click="$dispatch('shamar-open-confirm', { variant: 'danger', mode: 'confirm', title: 'Delete {{ resource.singularLabel }}?', message: 'This action cannot be undone.', confirmLabel: 'Delete', action: '{{ basePath }}/{{ resource.slug }}/{{ record.id }}/delete' })"
115
+ >Delete</button>
116
+ @endif
117
+ @else
118
+ @if(showEditButton)
119
+ <a href="{{ basePath }}/{{ resource.slug }}/{{ id }}/edit" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white rounded-md bg-fg-brand hover:opacity-90 transition-opacity">Edit</a>
120
+ @endif
121
+ @if(showBackToList)
122
+ <a href="{{ basePath }}/{{ resource.slug }}" data-shamar-list-href="{{ resource.slug }}" class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium border border-default text-body rounded-md hover:bg-surface-hover transition">Back to list</a>
123
+ @endif
124
+ @endif
125
+ @if(mode === 'edit' || mode === 'create' || mode === 'show')
126
+ @include('shamar::partials/record-pager')
127
+ @endif
128
+ </div>
129
+ </div>
130
+ </div>
@@ -0,0 +1,73 @@
1
+ @if(pagination)
2
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between text-sm text-body-subtle">
3
+ <p>
4
+ Page {{ pagination.page }} of {{ pagination.pageCount }}
5
+ · {{ pagination.total }} records
6
+ </p>
7
+
8
+ @if(pagination.pageCount > 1)
9
+ <div class="flex flex-wrap items-center gap-1.5">
10
+ @if(pagination.prevHref)
11
+ <a
12
+ href="{{ pagination.prevHref }}"
13
+ class="rounded-md border border-default bg-neutral-primary px-3 py-1.5 text-sm text-body hover:bg-surface-hover"
14
+ >Previous</a>
15
+ @else
16
+ <span class="rounded-md border border-default bg-neutral-primary/50 px-3 py-1.5 text-sm text-body-subtle cursor-not-allowed">Previous</span>
17
+ @endif
18
+
19
+ @each(link in pagination.links)
20
+ @if(link.type === 'ellipsis')
21
+ <span class="px-1.5 text-body-subtle select-none">…</span>
22
+ @else
23
+ <a
24
+ href="{{ link.href }}"
25
+ class="min-w-[2.25rem] rounded-md border px-2.5 py-1.5 text-center text-sm transition-colors {{ link.active ? 'border-brand bg-surface-inset font-medium text-fg-brand' : 'border-default bg-neutral-primary text-body hover:bg-surface-hover' }}"
26
+ {{ link.active ? 'aria-current="page"' : '' }}
27
+ >{{ link.label }}</a>
28
+ @endif
29
+ @endeach
30
+
31
+ @if(pagination.nextHref)
32
+ <a
33
+ href="{{ pagination.nextHref }}"
34
+ class="rounded-md border border-default bg-neutral-primary px-3 py-1.5 text-sm text-body hover:bg-surface-hover"
35
+ >Next</a>
36
+ @else
37
+ <span class="rounded-md border border-default bg-neutral-primary/50 px-3 py-1.5 text-sm text-body-subtle cursor-not-allowed">Next</span>
38
+ @endif
39
+
40
+ <form
41
+ method="get"
42
+ action="{{ pagination.formAction }}"
43
+ class="ml-1 flex items-center gap-1.5"
44
+ >
45
+ @if(query.search)
46
+ <input type="hidden" name="search" value="{{ query.search }}" />
47
+ @endif
48
+ @if(query.sort)
49
+ <input type="hidden" name="sort" value="{{ query.sort }}" />
50
+ <input type="hidden" name="direction" value="{{ query.direction }}" />
51
+ @endif
52
+ @if(query.perPage !== '15' && query.perPage !== 15)
53
+ <input type="hidden" name="perPage" value="{{ query.perPage }}" />
54
+ @endif
55
+ <label for="pagination-jump-page" class="sr-only">Jump to page</label>
56
+ <input
57
+ id="pagination-jump-page"
58
+ type="number"
59
+ name="page"
60
+ min="1"
61
+ max="{{ pagination.pageCount }}"
62
+ value="{{ pagination.page }}"
63
+ class="shamar-input w-16 px-2 py-1.5 text-center text-sm"
64
+ />
65
+ <button
66
+ type="submit"
67
+ class="rounded-md border border-default bg-neutral-primary px-2.5 py-1.5 text-sm text-body hover:bg-surface-hover"
68
+ >Go</button>
69
+ </form>
70
+ </div>
71
+ @endif
72
+ </div>
73
+ @endif
@@ -0,0 +1,37 @@
1
+ {{-- Cyclic prev/next record pager (PyVELM-style). Expects `recordPager`. --}}
2
+ @if(recordPager)
3
+ <div
4
+ class="inline-flex items-center gap-2 shrink-0"
5
+ data-shamar-record-pager
6
+ data-shamar-pager-prev="{{ recordPager.prevUrl }}"
7
+ data-shamar-pager-next="{{ recordPager.nextUrl }}"
8
+ >
9
+ <div class="inline-flex rounded-md border border-default overflow-hidden bg-neutral-primary">
10
+ <a
11
+ href="{{ recordPager.prevUrl }}"
12
+ class="inline-flex items-center justify-center w-8 h-8 text-body-subtle hover:bg-surface-hover hover:text-heading transition-colors"
13
+ title="Previous record (wraps at start)"
14
+ aria-label="Previous record"
15
+ data-shamar-pager-link
16
+ >
17
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
18
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5"/>
19
+ </svg>
20
+ </a>
21
+ <a
22
+ href="{{ recordPager.nextUrl }}"
23
+ class="inline-flex items-center justify-center w-8 h-8 text-body-subtle border-l border-default hover:bg-surface-hover hover:text-heading transition-colors"
24
+ title="Next record (wraps at end)"
25
+ aria-label="Next record"
26
+ data-shamar-pager-link
27
+ >
28
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2">
29
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5"/>
30
+ </svg>
31
+ </a>
32
+ </div>
33
+ <span class="text-xs text-body-subtle tabular-nums whitespace-nowrap" title="Position in the current list">
34
+ {{ recordPager.index }} / {{ recordPager.total }}
35
+ </span>
36
+ </div>
37
+ @endif