Agent skill

Chamilo Vue Base Components

by chamilo in chamilo/chamilo-lms

Swaps native form elements in Chamilo Vue files for Base components and checks that every Base, SectionHeader and Fieldset tag has its import.

GPL-3.0Auto-check: notesFrontend & Design

Install Chamilo Vue Base Components

skills CLI
$ npx skills add chamilo/chamilo-lms --skill use-base-components -a claude-code

Project install by default; add -g for ~/.claude/skills/.

GitHub CLI
$ gh skill install chamilo/chamilo-lms use-base-components --agent claude-code

Project scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).

Manual copy
$ git clone --depth 1 https://github.com/chamilo/chamilo-lms.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/use-base-components .claude/skills/use-base-components && rm -rf skills-src

Use ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.

Claude Code skills documentation · loads skills from .claude/skills/

Facts

Skill name
use-base-components
GitHub stars
1k
Token cost
~7.6k tokens
SKILL.md length
2,335 words
Files
1
Skills in repo
5
Repo updated
First seen
Licence
GPL-3.0

At a glance

Swaps native form elements in Chamilo Vue files for Base components and checks that every Base, SectionHeader and Fieldset tag has its import.

  • Works in 5 steps: For each native form element found → Add all required imports in alphabetical… → Remove any elements that were paired… → …
  • Creating a new Vue page, form or dialog in the Chamilo front end
  • SKILL.md covers ⚠ Critical rule (read first):…, Mapping: native element →…, Critical rule: label translation and Component APIs, plus 2 more sections
  • Calls yarn

What it does

Pointed at a Vue file (or the one currently open), the skill replaces native input, select, textarea and checkbox elements with the matching Base component from assets/vue/components/basecomponents/, following a mapping and rules written in the SKILL.md. It prefers the Base wrapper over the raw PrimeVue one, such as BaseDialog over Dialog and BaseSelect over Dropdown.

Its main warning concerns imports. In this project, Vue 3 script setup does not auto-register most local components, so BaseButton, BaseTable, SectionHeader, Fieldset and similar components must be imported explicitly, while a short list of PrimeVue components and BaseAppLink are registered globally in assets/vue/main.js and must not be.

A missing import does not break yarn build, which still reports success. It only shows up at runtime as a Failed to resolve component warning that CI does not catch, so the skill asks for a manual cross-check and ends with a verification command. It applies to Vue files only.

When your agent uses it

  • Creating a new Vue page, form or dialog in the Chamilo front end
  • Refactoring a view that still uses native input, select or textarea tags
  • Chasing a Failed to resolve component warning in the browser console
  • Adding SectionHeader or Fieldset to an existing Vue file

Example prompts

  • “Replace the native selects and text inputs in this Vue form with Base components and fix the imports.”
  • “The console shows Failed to resolve component: BaseDialog. Find and fix the missing imports.”
  • “Build a new settings page for course tools using the Base components and imports the project requires.”

Requirements

  • A Chamilo front-end checkout using Vue 3 and the basecomponents folder
  • Pre-approved tools (allowed-tools): Read, Edit, Write, Glob, Grep, Bash

Workflow steps

5 steps, taken from the first numbered list in SKILL.md.

  1. For each native form element found
  2. Add all required imports in alphabetical order alongside existing base component imports.
  3. Remove any elements that were paired with the replaced inputs.
  4. Remove wrapper elements that existed only to group the label+input pair, unless they
  5. Do not replace

What it can do on your machine

Read from SKILL.md and the folder at commit c3b9c05. It shows what the files ask for, not the result of running them.

  • Tool permissions

    Pre-approves these tools, so the agent can use them without asking each time:

    • Read
    • Edit
    • Write
    • Glob
    • Grep
    • Bash

    From allowed-tools in the SKILL.md frontmatter.

  • Runs code

    Shell commands in SKILL.md call:

    • yarn

    From the folder's file list and the shell code blocks in SKILL.md.

  • Network

    No URLs in SKILL.md. Its commands use yarn, which can reach the network depending on how they are called.

    From URLs in SKILL.md, links to its own repository left out.

  • Credentials

    Names no API keys, tokens, secrets or passwords.

    From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.

Context cost

Chamilo Vue Base Components loads about 7.6k tokens when it runs. Until then it costs about 202 tokens; SKILL.md has 2,335 words of instructions outside code blocks.

Always · name and description, kept in context so the agent knows when to use it
~202
When it runs · the whole SKILL.md, loaded when a task matches
~7.6k

Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.

Safety

Auto-check: notes

The automated check noted patterns worth knowing about, such as sudo or a known installer.

  • NotePre-approves every shell command (allowed-tools: Bash)SKILL.md
    allowed-tools: Read, Edit, Write, Glob, Grep, Bash

Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.

SKILL.md

The full file from chamilo/chamilo-lms at commit c3b9c05, republished under its GPL-3.0 licence (© chamilo). 2,335 words, ~7,570 tokens.

Download SKILL.mdSave it as .claude/skills/use-base-components/SKILL.md (or your agent's skills folder).
name
use-base-components
description
Replace native HTML form elements with Base* components in Vue files AND ensure every Base*/SectionHeader/Fieldset tag in any Vue file has a matching import in <script setup> (Vue 3 does not auto-register most local components in this project; see the global list below). Auto-invoke when: user creates ANY new Vue file or page (form, dialog, view), adds Base*/SectionHeader/Fieldset tags to an existing Vue file, edits a Vue file containing native <input>, <select>, <textarea>, or <checkbox> elements, asks to refactor form fields, reports "Failed to resolve component" runtime warnings, or mentions SectionHeader or any Base* component by name. Do NOT invoke for: non-Vue files, React or Angular components, styling-only changes inside files whose Base* imports are already complete.
allowed-tools
Read, Edit, Write, Glob, Grep, Bash

Review the Vue file(s) referenced in $ARGUMENTS (or the currently open file if no argument is given) and replace every native HTML form element with the appropriate Base* component from assets/vue/components/basecomponents/. Follow the mapping and rules below exactly.


⚠ Critical rule (read first): every component used in the template must be imported

In this project, Vue 3 with <script setup> does NOT auto-register most local components. A small set of components is registered globally in assets/vue/main.js (the app.component(...) calls) and must NOT be imported:

  • PrimeVue (global): Dialog, ConfirmDialog, DataView, Dropdown (alias of Select), InputText, Button, Column, ColumnGroup, Toolbar.
  • Base (global): BaseAppLink.

Everything else — BaseButton, BaseDialog, BaseSelect, BaseTable, BaseInputText, BaseInputNumber, BaseCheckbox, BaseCalendar, BaseAutocomplete, BaseMultiSelect, BaseRadioButtons, BaseColorPicker, BaseTextArea, BaseIcon, SectionHeader, Fieldset, etc. — MUST be imported explicitly in <script setup>.

Note: prefer the Base* wrapper over the raw PrimeVue global where one exists. Use BaseDialog (not the global Dialog), BaseSelect (not the global Dropdown), BaseInputText (not the global InputText), BaseButton (not the global Button). The PrimeVue globals exist mostly for legacy/transitional code and inside Base* wrappers. Column, ColumnGroup and Toolbar are the normal exceptions you use directly (inside BaseTable).

Why this rule exists: when a non-global component is missing its import, Vue 3 does NOT raise a build error. yarn build reports webpack compiled successfully even with missing imports. The failure surfaces only at runtime as [Vue warn]: Failed to resolve component: BaseDialog, and that warning does not show in CI. So you cannot rely on yarn build to catch this — you must cross-check by hand (or via the verification command at the end of this skill).

When you create a new Vue file or add a component tag to an existing one, the imports are part of the same change.

Standard import paths (note: from assets/vue/views/<feature>/Foo.vue the prefix is ../../; from assets/vue/views/<feature>/<sub>/Foo.vue it is ../../../):

js
// Most common Base* components
import BaseAutocomplete from "../../components/basecomponents/BaseAutocomplete.vue"
import BaseButton       from "../../components/basecomponents/BaseButton.vue"
import BaseCalendar     from "../../components/basecomponents/BaseCalendar.vue"
import BaseCheckbox     from "../../components/basecomponents/BaseCheckbox.vue"
import BaseColorPicker  from "../../components/basecomponents/BaseColorPicker.vue"
import BaseDialog       from "../../components/basecomponents/BaseDialog.vue"
import BaseIcon         from "../../components/basecomponents/BaseIcon.vue"
import BaseInputNumber  from "../../components/basecomponents/BaseInputNumber.vue"
import BaseInputText    from "../../components/basecomponents/BaseInputText.vue"
import BaseMultiSelect  from "../../components/basecomponents/BaseMultiSelect.vue"
import BaseRadioButtons from "../../components/basecomponents/BaseRadioButtons.vue"
import BaseSelect       from "../../components/basecomponents/BaseSelect.vue"
import BaseTable        from "../../components/basecomponents/BaseTable.vue"
import BaseTextArea     from "../../components/basecomponents/BaseTextArea.vue"

// Layout
import SectionHeader from "../../components/layout/SectionHeader.vue"

// PrimeVue Fieldset is NOT global — import it when used
import Fieldset from "primevue/fieldset"

Exceptions (do NOT import): the global components listed above — Dialog, ConfirmDialog, DataView, Dropdown, InputText, Button, Column, ColumnGroup, Toolbar, BaseAppLink. In practice the ones you'll legitimately use directly are Column / ColumnGroup / Toolbar (inside <BaseTable>).


Mapping: native element → Base* component

Native elementBase* componentImport path
<input type="text">BaseInputText../../components/basecomponents/BaseInputText.vue
<textarea>BaseTextArea../../components/basecomponents/BaseTextArea.vue
<input type="number">BaseInputNumber../../components/basecomponents/BaseInputNumber.vue
<select> + <option>BaseSelect../../components/basecomponents/BaseSelect.vue
<input type="date">BaseCalendar../../components/basecomponents/BaseCalendar.vue
<input type="checkbox"> (binary)BaseCheckbox../../components/basecomponents/BaseCheckbox.vue
<input type="radio"> groupBaseRadioButtons../../components/basecomponents/BaseRadioButtons.vue
Multi-select listBaseMultiSelect../../components/basecomponents/BaseMultiSelect.vue
Search-with-suggestionsBaseAutocomplete../../components/basecomponents/BaseAutocomplete.vue
<input type="color">BaseColorPicker../../components/basecomponents/BaseColorPicker.vue

Remove the surrounding <div><label>…</label><input>…</div> wrapper — every Base* component renders its own label internally.


Critical rule: label translation

Components differ in whether they call t() internally:

ComponentInternal t()?How to pass label
BaseTextAreaYes ({{ t(label) }})label="Raw key" — never wrap in t()
All othersNo:label="t('Raw key')" — always wrap in t()

Passing t('Key') to BaseTextArea will attempt to translate an already-translated string and break non-English locales.


Component APIs

BaseInputText
vue
<BaseInputText
  id="field-id"
  v-model="form.title"
  :label="t('Title')"
  name="title"
/>

Props: id (required), label (required), modelValue (String|null, required), name, errorText, isInvalid, required, helpText, formSubmitted, disabled. Has inheritAttrs: false with v-bind="$attrs" on the inner input → extra attrs and events (@input, @blur, autocomplete, placeholder, etc.) are forwarded.


BaseTextArea
vue
<BaseTextArea
  id="field-id"
  v-model="form.description"
  label="Description"
  name="description"
  rows="3"
/>

Props: id (required), label (required, raw key), modelValue (String, default ""), errorText, isInvalid. Extra attrs (rows, name, etc.) forwarded via v-bind="$attrs".


BaseInputNumber
vue
<BaseInputNumber
  id="field-id"
  v-model="form.score"
  :label="t('Score')"
  :min="0"
  :step="0.1"
  name="score"
/>

Props: id (required), label (required), modelValue (Number, required), step (default 1), min, max, isInvalid, errorText, disabled, helpText. Renders PrimeVue InputNumber with +/− spinner buttons. name is NOT forwarded — no v-bind="$attrs" on this component.


BaseSelect
vue
<BaseSelect
  id="field-id"
  v-model="form.status"
  :label="t('Status')"
  :options="statusOptions"
  name="status"
/>

Props: id, label (required), options (Array, required), optionLabel (default "label"), optionValue (default "value"), name, placeholder, allowClear, hastEmptyValue, isLoading, disabled, messageText, isInvalid. Uses defineModel — compatible with v-model.

Options format (default keys):

js
const statusOptions = [
  { label: t('Active'), value: 'active' },
  { label: t('Inactive'), value: 'inactive' },
]

Or pass :option-label="'title'" :option-value="'id'" to use existing object keys directly.

Properties with @ in the name (e.g. @id) are not safe as optionValue strings — map them to plain keys in a computed property first:

js
const compensationOptions = computed(() =>
  compensations.value.map((c) => ({ label: c.title, value: c['@id'] }))
)

Use allow-clear for optional filters (adds a clear/× button). Use :hast-empty-value="true" to prepend a -- row when the field is required with a blank default.

For large lists (dozens/hundreds of options) or nested lists, use BaseAutocomplete instead of BaseSelect — see the BaseAutocomplete section and the caching pattern with useEntityCache.


BaseCalendar
vue
<BaseCalendar
  id="field-id"
  v-model="form.startDate"
  :label="t('Start date')"
/>

Props: id (required), label (required), type ("single" | "range", default "single"), showTime, isInvalid, errorText, showInline. Uses defineModel — type Date | Array | String | null.

⚠ Important:

  • Initialise the model value as null or new Date(), never as "".
  • The model holds a Date object after user interaction. Serialise with new Date(value).toISOString() before sending to the API.
  • For string comparisons in computed filters, convert first:
    js
    const dateStr = value instanceof Date ? value.toISOString().slice(0, 10) : String(value).slice(0, 10)
  • After switching from <input type="date">, update resetFilters / initial form values to use null instead of "".

Date range — type="range": When a form has a start field and an end field, use a single BaseCalendar with type="range" instead of two separate calendars. The model is [startDate, endDate | null]:

vue
<BaseCalendar
  id="date-range"
  v-model="dateRange"
  :label="t('Assignment period')"
  type="range"
/>
js
const dateRange = ref([new Date(), null])   // initialize with start = today, end = null

// When saving, extract by index:
const payload = {
  startDate: dateRange.value?.[0] ? new Date(dateRange.value[0]).toISOString() : null,
  endDate:   dateRange.value?.[1] ? new Date(dateRange.value[1]).toISOString() : null,
}

// When resetting:
dateRange.value = null   // or [new Date(), null] if you want to pre-fill the start

The second element is null until the user selects the end date — check dateRange.value?.[1] before serializing.


BaseCheckbox
vue
<BaseCheckbox
  id="field-id"
  v-model="form.isActive"
  :label="t('Active')"
  name="is_active"
/>

Props: id (required), name (required), label (required). Uses defineModel (Boolean) — binary checkbox only. For multi-value checkbox arrays (e.g. selecting multiple IRIs), keep native <input type="checkbox"> with v-model array binding.


BaseRadioButtons
vue
<BaseRadioButtons
  v-model="form.type"
  :options="typeOptions"
  :title="t('Type')"
  name="type"
/>

Props: modelValue (String|Number, required), name (required), options (Array { label, value }, required), title (optional heading). Uses traditional emit pattern (not defineModel) — v-model works as expected. Translate option labels in the computed/data, not inside the component:

js
const typeOptions = computed(() => [
  { label: t('Internal'), value: 'internal' },
  { label: t('External'), value: 'external' },
])

BaseMultiSelect
vue
<BaseMultiSelect
  v-model="form.tags"
  :options="tagOptions"
  :label="t('Tags')"
  input-id="field-id"
  option-label="title"
  option-value="id"
/>

Props: modelValue (Array, required), options (Array), inputId (required — note: inputId not id), label (required), optionLabel (default "name"), optionValue (default "id"), isInvalid, errorText, isLoading. Renders chips for selected values.


BaseAutocomplete
vue
<BaseAutocomplete
  id="field-id"
  v-model="form.user"
  :label="t('User')"
  :search="searchUsers"
  option-label="fullName"
/>

Props: id (required), label (required), search (Function, required), optionLabel (default "name"), isMultiple, disabled, helpText, isInvalid.

When to use BaseAutocomplete instead of BaseSelect:

  • The list has dozens or hundreds of items — a <select> becomes unwieldy.
  • The source is a paginated endpoint or has no known upper bound (e.g. /api/users).
  • The list is nested (e.g. skill tree) and the user needs to search by text.

For short and fixed lists (≤ ~20 items, e.g. statuses, periodicities, types), keep using BaseSelect.

The search prop receives the query and returns a Promise with the array of suggestions. There are two patterns depending on the dataset size:

1) Server-side search — large or paginated datasets:

js
async function searchUsers(query) {
  const result = await baseService.getCollection('/api/users', { search: query, itemsPerPage: 10 })
  return result.items
}

2) In-memory cached search — useEntityCache (assets/vue/composables/useEntityCache.js):

Factory with a module-level Map. Caches the list by endpoint for the whole session — multiple views / multiple autocomplete instances share a single request.

js
import { useEntityCache } from "../../composables/useEntityCache"

const skillsCache = useEntityCache("/api/skills", { pagination: false })

onMounted(async () => {
  await skillsCache.load()   // loads once per session
})
vue
<BaseAutocomplete
  id="skill-ref"
  v-model="selectedSkill"
  :label="t('Skill')"
  :search="skillsCache.search"
  option-label="title"
/>

Cache API:

  • load() — lazy-loads once; deduplicates concurrent calls; subsequent calls return the already cached items.
  • search(query) — filters in-memory by the labelField (3rd argument of the factory, default "title"). Pass as the search prop.
  • findById(id) — resolves a numeric id to the object. Useful in edit mode when the backend sends only the id.
  • invalidate() — clears the cache. Call it after creating/editing/deleting the entity to force a reload.

When the v-model must be sent as an id (not an IRI nor an object) — BaseAutocomplete keeps the full object in the model; on save extract .id, and on load (edit) resolve id → object with findById:

js
// addItem
form.items.push({ ref: null })

// load (edit mode)
items: backendItems.map((i) => ({ ref: skillsCache.findById(i.refId) }))

// save payload
items: form.items.filter((i) => i.ref?.id).map((i) => ({ refId: Number(i.ref.id) }))

Backend — enable pagination=false when using the cache: entities whose ApiResource does not declare paginationClientEnabled: true will silently ignore the client's pagination: false and return only 30 items. Verify the entity attribute before caching full lists.

Always use BaseAutocomplete instead of the manual <BaseInputText> + <ul> pattern. When you see this pattern in a Vue file, replace it:

vue
<!-- ❌ manual pattern to replace -->
<div class="relative">
  <BaseInputText v-model="userSearch" @input="onUserInput" ... />
  <ul v-if="results.length" class="absolute ...">
    <li v-for="u in results" @click="selectUser(u)">{{ u.fullName }}</li>
  </ul>
</div>
<p v-if="selectedUser">{{ selectedUser.fullName }}</p>
vue
<!-- ✅ use BaseAutocomplete -->
<BaseAutocomplete
  id="field-id"
  v-model="selectedUser"
  :label="t('User')"
  :search="searchUsers"
  option-label="fullName"
/>

Note: this project also ships BaseUserFinder and BaseSearchSelect for some user/entity pickers. If the file already uses one of them, keep it; for new generic autocompletes prefer BaseAutocomplete.

When the v-model must be an IRI (not the full object), keep a separate ref for the autocomplete and extract the IRI when building the payload:

js
const selectedUser = ref(null)

// in save():
const payload = { user: selectedUser.value?.["@id"] ?? null }

When clearing or resetting, simply assign null:

js
function resetForm() {
  selectedUser.value = null
}

Also remove: the text refs (userSearch), the suggestion refs (userSearchResults), the debounce timers, and the onUserInput, selectUser, clearUser functions.


BaseColorPicker
vue
<BaseColorPicker
  v-model="form.color"
  :label="t('Color')"
/>

Props: modelValue (Color instance from colorjs.io, required), label (no internal t() — use :label="t('...')"), error.

⚠ Important: The model value must be a Color object from colorjs.io, never a plain string. Three places in the script always need updating together:

js
import Color from 'colorjs.io'

// 1. Initial state (ref declaration)
const form = ref({ color: new Color('#3B82F6') })

// 2. Loading existing data (e.g. openForm(item))
form.value.color = item ? new Color(item.color) : new Color('#3B82F6')

// 3. Serialising for the API (save payload)
const payload = {
  color: form.value.color.toString({ format: 'hex' }),
}

colorjs.io is already a project dependency — no installation needed.


BaseDialog
vue
<BaseDialog
  v-model:is-visible="myDialog"
  :title="editingItem ? t('Edit item') : t('Add item')"
  :style="{ width: '480px' }"
>
  <!-- form fields -->
  <template #footer>
    <BaseButton :label="t('Cancel')" icon="close" type="plain" @click="myDialog = false" />
    <BaseButton :label="t('Save')" icon="save" type="success" @click="save" />
  </template>
</BaseDialog>

Props: title (String, required), headerIcon (String, optional — MDI icon name). Model: isVisible (Boolean) — bind with v-model:is-visible. Always use <BaseDialog> instead of PrimeVue <Dialog> directly (even though Dialog is globally registered). It wraps Dialog with modal: true and a consistent header layout.

  • Extra attrs (e.g. :style, :class) fall through to the inner <Dialog> — use :style="{ width: '...' }" to control dialog width.
  • Footer goes in the #footer named slot.
  • Never import Dialog from primevue/dialog in a view — use BaseDialog instead.
  • For simple confirm/cancel or delete dialogs, BaseDialogConfirmCancel and BaseDialogDelete already exist — prefer them over hand-rolling a BaseDialog with two buttons.

Show full SKILL.md (944 more words)Show less
BaseTable
vue
<BaseTable :values="rows" :is-loading="isLoading">
  <Column field="firstname" :header="t('First name')" sortable />
</BaseTable>

BaseTable (assets/vue/components/basecomponents/BaseTable.vue) wraps PrimeVue DataTable. Column is globally registered in main.js — no import needed.

Server-side pagination (lazy mode)

Use lazy mode when the underlying API supports native pagination and the dataset may exceed the default page size (30 for API Platform). For short, bounded lists prefer client-side with { pagination: false } — see assets/vue/views/lp/LpList.vue or assets/vue/views/message/MessageList.vue for that variant. For the lazy variant, see assets/vue/views/admin/UserList.vue or assets/vue/views/admin/CourseList.vue.

When to choose lazy (server-side):

  • Collection has no known upper bound or is expected to grow (users, sessions, applications).
  • The API Platform resource exposes pagination (default behaviour unless overridden).

When to choose { pagination: false } (client-side):

  • Bounded list ≤ ~50 items (statuses, types, branches, periodicities used in selects).
  • The entity declares paginationClientEnabled: true so the client can opt out.

Lazy pagination template:

vue
<BaseTable
  v-model:rows="pageSize"
  :is-loading="loading"
  :lazy="true"
  :total-items="total"
  :values="items"
  data-key="id"
  @page="onPage"
>
  <Column :header="t('Title')" field="title" />
</BaseTable>
js
import baseService from "../../services/baseService"

const items = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)

async function load() {
  loading.value = true
  try {
    const { items: rows, totalItems } = await baseService.getCollection("/api/periodicities", {
      page: page.value,
      itemsPerPage: pageSize.value,
    })
    items.value = rows
    total.value = totalItems
  } finally {
    loading.value = false
  }
}

function onPage(event) {
  page.value = event.page + 1   // PrimeVue uses 0-based pages; API Platform uses 1-based
  pageSize.value = event.rows
  load()
}

onMounted(load)

Convention — where the request lives:

  • Call baseService.getCollection(endpoint, params) directly from the view when you need pagination metadata (totalItems). It already returns { items, totalItems, nextPageParams }.
  • The service wrapper's getAll(params) is the shortcut "give me the items array only". Do not add getPage / getPaginated / getCollection variants to service wrappers — the base service already exposes that entry point.
  • Reserve the service wrapper for endpoint-specific shortcuts (getAll, create, update, remove, and any non-trivial composed queries).

sortable columns + lazy = need backend support. With :lazy="true" PrimeVue stops sorting client-side. Marking a column sortable is then only meaningful when the entity declares #[ApiFilter(OrderFilter::class, properties: [...])]. If the filter is missing, remove sortable to avoid a misleading UI; re-enable once OrderFilter is wired and you forward order[field]=asc|desc from a @sort handler.


BaseButton
vue
<BaseButton :label="t('Save')" icon="save" type="success" @click="save" />

Always use <BaseButton> instead of a plain <button> (or the global PrimeVue Button). Import from ../../components/basecomponents/BaseButton.vue.

CRUD color convention (type prop):

  • Create / add / save / import → type="success" (green)
  • Read / view / export / list → type="primary" (blue)
  • Update / edit / configure / move → type="secondary" (orange)
  • Delete / disable / remove → type="danger" (red)
  • Cancel / dismiss → type="plain" (gray)
  • Buttons are for actions only — never style a non-action link as a button.

Table row action convention:

  • Edit: type="secondary-text", icon="pencil", only-icon, size="small"
  • Delete: type="danger-text", icon="delete", only-icon, size="small"
  • Never put ch-tool-icon on icons inside a button — the icon inherits the button's text colour automatically.

Fieldset (field grouping)

When a group of fields has a visible title, use PrimeVue's <Fieldset> instead of <div class="field-group"> + <label>. Fieldset is not global — import it.

vue
import Fieldset from "primevue/fieldset"

<Fieldset :legend="t('Tags')">
  <div class="flex flex-wrap gap-4">
    <BaseCheckbox ... />
  </div>
</Fieldset>

When to apply it: any <div> with a <label> whose only purpose is to title a group of inputs (checkboxes, radios, related fields). Do not use it for a single field — every Base* component already renders its own label.


BaseAdvancedSettingsButton (collapsible "Advanced settings" section)
vue
import BaseAdvancedSettingsButton from "../../components/basecomponents/BaseAdvancedSettingsButton.vue"

<BaseAdvancedSettingsButton v-model="showAdvancedSettings">
  <div class="flex flex-col gap-4">
    <BaseInputText id="phone" v-model="form.phone" :label="t('Phone number')" name="phone" />
    <!-- rest of the advanced/optional fields -->
  </div>
</BaseAdvancedSettingsButton>

Props: modelValue (Boolean, required) — bind with v-model, own state as a plain ref(false) (collapsed by default). Content goes in the default slot — the component renders it itself (in a rounded border-gray-25 bg-gray-10 panel) only when expanded; don't also wrap it in your own v-if.

When to use it: any form that hides a block of optional/rarely-needed fields behind a toggle (a legacy page's own "Advanced settings" panel, extra fields, rarely-changed options). This is the only correct component for that pattern in this project — reference usages: course/Form.vue, links/LinkForm.vue, documents/Form.vue, glossary/GlossaryForm.vue, attendance/AttendanceForm.vue, lp/LpCertificateForm.vue.

Do not reach for a raw PrimeVue <Fieldset :toggleable="true" collapsed> for this — it renders a bordered box even while collapsed and its label styling doesn't match. This is an easy trap: Fieldset toggles superficially "look like" the same pattern, so it's worth actively checking for a purpose-built Base*/feature component before defaulting to a generic PrimeVue one — this project has more of them than any single skill section documents.


SectionHeader

Use <SectionHeader> for section or page headings instead of a manual <h2> / <div>.

vue
import SectionHeader from "../../components/layout/SectionHeader.vue"

<SectionHeader :title="t('Benefit tags')">
  <BaseButton
    :label="t('Add tag')"
    icon="plus"
    type="success"
    @click="openForm()"
  />
</SectionHeader>

Props: title (String, required), size (String, default "2" → renders <h2>). The default slot is for action buttons (rendered to the right of the title). Automatically includes StudentViewButton when there is course context — do not add it manually.


Notifications — useNotification

Never use PrimeVue's useToast directly. Always use the composable assets/vue/composables/notification.js:

js
import { useNotification } from "../../composables/notification"

const { showSuccessNotification, showErrorNotification } = useNotification()

// success
showSuccessNotification(t("Saved"))

// error from an Error object (Axios or native JS)
showErrorNotification(e)

// error with a fixed message (when the catch doesn't have the correct message)
showErrorNotification(t("Cannot delete a benefit that has active assignments."))

showErrorNotification accepts both an Error object / Axios response and a string. It sanitizes the message, filters internal exception leakage, and prevents duplicate toasts.

Available methods: showSuccessNotification, showInfoNotification, showWarningNotification, showErrorNotification.


Checklist

For each target file:

  1. For each native form element found: a. Identify the correct Base* component from the mapping above. b. Note the existing v-model, name, and any event handlers (@input, @change, etc.). c. Apply the label translation rule (raw key for BaseTextArea, t() for all others). d. Preserve name attributes — Playwright tests target inputs by name. e. For BaseCalendar replacements, check initialization values and filter comparisons. f. For BaseSelect replacements, prepare an options computed if the source data uses non-standard or unsafe property names (like @id).
  2. Add all required imports in alphabetical order alongside existing base component imports.
  3. Remove any <label> elements that were paired with the replaced inputs.
  4. Remove wrapper <div> elements that existed only to group the label+input pair, unless they carry layout classes needed by the surrounding flex/grid container.
  5. Do not replace:
    • <input type="checkbox"> used in v-model array bindings (multi-value selection).
    • <input type="color"> unless the form already imports colorjs.io or it is trivial to add.
    • Inputs inside third-party component slots that require a native element.

Final verification — cross-check template tags vs <script setup> imports

Before declaring the file done, run this check on EVERY Vue file you created or modified. yarn build will compile successfully even when imports are missing, so this is the only reliable gate.

bash
file=assets/vue/views/path/to/YourFile.vue

# Components used in the template (Base*, PrimeVue PascalCase, layout):
used=$(grep -oE '<(Base[A-Z][A-Za-z0-9]+|SectionHeader|Fieldset|Column)\b' "$file" \
  | sed 's/^<//' | sort -u)

# Components imported in <script setup>:
imported=$(grep -oE '^import [A-Z][A-Za-z0-9]+ from' "$file" \
  | awk '{print $2}' | sort -u)

# Globally-registered components (app.component calls in main.js) — never need an import.
# One per line and quoted below, so it works in both bash and zsh (zsh does NOT
# word-split unquoted variables, so a space-separated list would break here):
globals="BaseAppLink
Button
Column
ColumnGroup
ConfirmDialog
DataView
Dialog
Dropdown
InputText
Toolbar"

# Anything in `used` minus `imported` minus globals is a missing import:
echo "USED:"; echo "$used"
echo "IMPORTED:"; echo "$imported"
echo "MISSING:"; comm -23 <(echo "$used") <(printf '%s\n' "$imported" "$globals" | sort -u)

If MISSING is non-empty, add the imports before returning. Re-run yarn build after adding them as a sanity check, but remember that build success alone does NOT prove the imports are correct — only the diff above does.

Common offenders observed in this project: BaseDialog, BaseSelect, BaseTable, BaseAutocomplete, BaseInputNumber, SectionHeader. These are easy to forget because their tags read as if they were globally registered (<BaseDialog>, <SectionHeader>).

© chamilo, GPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file

Files

Just SKILL.md in .claude/skills/use-base-components of chamilo/chamilo-lms.

Open the folder on GitHubat commit c3b9c05

Compare with similar skills

Chamilo Vue Base Components next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.

Chamilo Vue Base Components compared with similar skills
SkillStarsUsed inTokensAuto-checkLicenceRepo updated
Chamilo Vue Base Components this skillchamilo/chamilo-lms1k—~7.6kAutomated safety check: NotesGPL-3.0
Buefy Vue UI Componentsbuefy/buefy9.5k—~8.2kAutomated safety check: PassMIT
Frontend Developmentavibebuilder/claude-prime120—~1.3kAutomated safety check: PassMIT
GSAP in Vue, Nuxt and Sveltegreensock/gsap-skills16k4 repos~2.6kAutomated safety check: PassMIT
Vuetify Playgroundvuetifyjs/vuetify41k—~730Automated safety check: PassCustom licence
Formkitformkit/formkit4.8k—~1.7kAutomated safety check: PassMIT

Similar skills

  • Rules for writing Vue 3 templates with Buefy components on Bulma CSS: install options, prop conventions, b-field wrapping, tables, theming and dialogs.

    9.5k GitHub stars~8.2k tokensUpdated 28 days ago
    Frontend & DesignAuto-check passed
  • Frontend Development

    avibebuilder/claude-prime

    A skill your agent uses for ANY work involving React, Next.js, TypeScript, or Tailwind in the browser layer.

    120 GitHub stars~1.3k tokensUpdated 4 mo ago
    Frontend & DesignAuto-check passed
  • GSAP in Vue, Nuxt and Svelte

    greensock/gsap-skills

    Shows how to use GSAP in Vue, Nuxt, Svelte and other lifecycle-based frameworks: create after mount, scope selectors to the component, and revert on unmount.

    16k GitHub starsUsed in 4 repos~2.6k tokens
    Frontend & DesignAuto-check passed
  • Vuetify Playground

    vuetifyjs/vuetify

    Maintains the Vuetify repo's local Playground.vue so contributors get a realistic reproduction and a short demo they can paste into a pull request description.

    41k GitHub stars~730 tokensUpdated today
    Frontend & DesignAuto-check passed
  • Formkit

    formkit/formkit

    A skill your agent uses when working with FormKit forms, validation, schema, or custom inputs in React, Vue, or Nuxt projects.

    4.8k GitHub stars~1.7k tokensUpdated 2 mo ago
    Frontend & DesignAuto-check passed
  • Uncodixfy

    pdsuwwz/chatgpt-vue3-light-mvp

    Prevents generic AI/Codex UI patterns when generating frontend code.

    578 GitHub starsUsed in 2 repos~3k tokens
    Frontend & DesignAuto-check passed

More from chamilo/chamilo-lms

  • Chamilo Theme From Site

    chamilo/chamilo-lms

    Creates a Chamilo color theme with a logo from a website's branding or from a logo file's dominant colors, entirely through the Chamilo REST API.

    1k GitHub stars~5.1k tokensUpdated today
    Auto-check passed
  • Chamilo Changelog Updater

    chamilo/chamilo-lms

    Adds commits for a new Chamilo release to the changelog page, classifying them into categories and skipping those already listed for that version.

    1k GitHub stars~4.2k tokensUpdated today
    Auto-check passed
  • Vue Route Breadcrumbs

    chamilo/chamilo-lms

    Gives a route in Chamilo's Vue app its breadcrumb entirely from router meta fields, without editing the Breadcrumb component or naming pages.

    1k GitHub stars~2k tokensUpdated today
    Auto-check passed
  • Chamilo Feature Test Author

    chamilo/chamilo-lms

    Turns a plain-language Chamilo feature description into a Playwright and Gherkin test suite confirmed against the live app, not just the source code.

    1k GitHub stars~3.6k tokensUpdated today
    Auto-check passed

Works with

Questions about Chamilo Vue Base Components

What does Chamilo Vue Base Components do?

Swaps native form elements in Chamilo Vue files for Base components and checks that every Base, SectionHeader and Fieldset tag has its import. md. It prefers the Base wrapper over the raw PrimeVue one, such as BaseDialog over Dialog and BaseSelect over Dropdown.

When should I use Chamilo Vue Base Components?

Chamilo Vue Base Components fits situations like: creating a new Vue page, form or dialog in the Chamilo front end; refactoring a view that still uses native input, select or textarea tags; chasing a Failed to resolve component warning in the browser console; adding SectionHeader or Fieldset to an existing Vue file.

How do I install Chamilo Vue Base Components in Claude Code?

Run `npx skills add chamilo/chamilo-lms --skill use-base-components -a claude-code`. Or copy the skill folder (.claude/skills/use-base-components in chamilo/chamilo-lms) into .claude/skills/use-base-components in your project. Claude Code loads it when a task matches its description.

How do I install Chamilo Vue Base Components in Codex?

Run `npx skills add chamilo/chamilo-lms --skill use-base-components -a codex`. Or copy the skill folder (.claude/skills/use-base-components in chamilo/chamilo-lms) into .agents/skills/use-base-components in your project. Codex loads it when a task matches its description.

Can I use Chamilo Vue Base Components in Cursor, Gemini CLI or GitHub Copilot?

Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add chamilo/chamilo-lms --skill use-base-components -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/use-base-components, .gemini/skills/use-base-components, .github/skills/use-base-components and .opencode/skills/use-base-components in your project.

What does Chamilo Vue Base Components need to run?

Going by SKILL.md and its folder, Chamilo Vue Base Components needs the command-line tools its instructions call (yarn). Our summary lists: A Chamilo front-end checkout using Vue 3 and the basecomponents folder. Its frontmatter pre-approves these tools: Read, Edit, Write, Glob, Grep, Bash.

Does Chamilo Vue Base Components access the network?

SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.

Is Chamilo Vue Base Components safe to install?

Our automated static check of SKILL.md found notes only (pre-approves every shell command (allowed-tools: bash)), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.

What licence does Chamilo Vue Base Components use?

Chamilo Vue Base Components is published under the GPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.

How many tokens does Chamilo Vue Base Components use?

About 7.6k tokens (SKILL.md is roughly 30k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.

What are the alternatives to Chamilo Vue Base Components?

Skills that share tags, products or a category with Chamilo Vue Base Components: Buefy Vue UI Components (buefy/buefy, 9.5k stars), Frontend Development (avibebuilder/claude-prime, 120 stars), GSAP in Vue, Nuxt and Svelte (greensock/gsap-skills, 16k stars) and Vuetify Playground (vuetifyjs/vuetify, 41k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.

Who maintains Chamilo Vue Base Components?

chamilo (a GitHub organization) maintains it in chamilo/chamilo-lms, which has 1,008 GitHub stars. The repository holds 5 skills in this directory. The repository was last updated on October 6, 2026.

Source: chamilo/chamilo-lms on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.